trunks: CRUD API, PJSIP trunk config, live registration status, Trunks page
voip.ms-first trunk form with the portal checklist, POP server list and Test button; passwords are write-only and encrypted. Registration and IP-auth trunks render per 03 §5.3 (TCP/TLS add ;transport=). The API refuses a server name that doesn't resolve and transports that are off, because either makes Asterisk drop the trunk. Live state: registration from PJSIPShowRegistrationsOutbound (re-read on every Registry event: two sub-accounts on one server share Username/Domain), reachability from PJSIPShowEndpoint and trunk ContactStatus (static contacts aren't in PJSIPShowContacts). Trunk state is admin-only. Dashboard gets a "Trunks up" card. trunk-test.ts (in pnpm test:asterisk) runs a fake voip.ms (second Asterisk): the good trunk registers, a wrong password is Rejected, qualify reaches the provider; its recorded AMI drives the unit tests. scripts/e2e/m3-t1.mjs does the same through the UI. The acceptance check with a real voip.ms sub-account is still to do. Tests: settle() now awaits an apply instead of a fixed sleep (flaked under full-suite load). Task: M3-T1
This commit is contained in:
1 parent
c0c4036887
commit
1b7f60b5f2
48 files changed
+2749
-33
No files matched your search
@@ -1,19 +1,22 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Live data from /api/live (05 §1): one WebSocket per tab, reconnecting with backoff.
|
||||
import { create } from 'zustand';
|
||||
import type { ExtensionLive, LiveMessage } from '@selfphoneng/shared';
|
||||
import type { ExtensionLive, LiveMessage, TrunkLive } from '@selfphoneng/shared';
|
||||
|
||||
type LiveStore = {
|
||||
/** WebSocket state: 'connecting' before the first snapshot. */
|
||||
status: 'connecting' | 'open' | 'reconnecting';
|
||||
amiConnected: boolean;
|
||||
extensions: Record<string, ExtensionLive>;
|
||||
/** Admins only (empty for users). */
|
||||
trunks: Record<string, TrunkLive>;
|
||||
};
|
||||
|
||||
export const useLive = create<LiveStore>(() => ({
|
||||
status: 'connecting',
|
||||
amiConnected: false,
|
||||
extensions: {},
|
||||
trunks: {},
|
||||
}));
|
||||
|
||||
function apply(m: LiveMessage) {
|
||||
@@ -24,6 +27,7 @@ function apply(m: LiveMessage) {
|
||||
status: 'open',
|
||||
amiConnected: m.data.ami.connected,
|
||||
extensions: Object.fromEntries(m.data.extensions.map((e) => [e.number, e])),
|
||||
trunks: Object.fromEntries((m.data.trunks ?? []).map((t) => [t.trunkId, t])),
|
||||
};
|
||||
case 'extension.updated':
|
||||
return { extensions: { ...s.extensions, [m.data.number]: m.data } };
|
||||
@@ -31,6 +35,12 @@ function apply(m: LiveMessage) {
|
||||
const { [m.data.number]: _gone, ...rest } = s.extensions;
|
||||
return { extensions: rest };
|
||||
}
|
||||
case 'trunk.updated':
|
||||
return { trunks: { ...s.trunks, [m.data.trunkId]: m.data } };
|
||||
case 'trunk.removed': {
|
||||
const { [m.data.trunkId]: _gone, ...rest } = s.trunks;
|
||||
return { trunks: rest };
|
||||
}
|
||||
case 'ami.status':
|
||||
return { amiConnected: m.data.connected };
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user