diff --git a/.env.example b/.env.example
index 486bd1a..1b64d3b 100644
--- a/.env.example
+++ b/.env.example
@@ -25,6 +25,7 @@ SIP_6002_PASSWORD=
SIP_6003_PASSWORD=
SIP_6101_PASSWORD=
SIP_6102_PASSWORD=
+SIP_6103_PASSWORD=
# IP/DNS сервера, который WebRTC-клиенты видят в локальной сети. При запуске
# только на той же машине оставьте 127.0.0.1; RTP-порты должны отображаться 1:1.
SIP_EXTERNAL_MEDIA_ADDRESS=127.0.0.1
diff --git a/Makefile b/Makefile
index ca17c3b..58e5f4c 100644
--- a/Makefile
+++ b/Makefile
@@ -47,7 +47,7 @@ sip: ## Собрать и поднять локальный Asterisk SIP/VoIP
offline-sip: ## Поднять SIP из уже собранного образа без сети
$(COMPOSE) -f docker-compose.yml -f docker-compose.sip.yml up --pull never --no-build -d sip
-sip-credentials: ## Показать локальные пароли софтфонов 6001–6003 и WebRTC 6101–6102
+sip-credentials: ## Показать локальные пароли софтфонов 6001–6003 и WebRTC 6101–6103
$(COMPOSE) -f docker-compose.yml -f docker-compose.sip.yml exec -T sip cat /var/lib/lct-sip/credentials.env
sip-test: ## Проверить регистрацию, звонок, RTP и задержку (args="--rtp-host-offset 1000")
diff --git a/docker-compose.sip.yml b/docker-compose.sip.yml
index 25f5160..79d23e6 100644
--- a/docker-compose.sip.yml
+++ b/docker-compose.sip.yml
@@ -9,6 +9,7 @@ services:
SIP_6003_PASSWORD: ${SIP_6003_PASSWORD:-}
SIP_6101_PASSWORD: ${SIP_6101_PASSWORD:-}
SIP_6102_PASSWORD: ${SIP_6102_PASSWORD:-}
+ SIP_6103_PASSWORD: ${SIP_6103_PASSWORD:-}
SIP_EXTERNAL_MEDIA_ADDRESS: ${SIP_EXTERNAL_MEDIA_ADDRESS:-127.0.0.1}
SIP_RTP_START: ${SIP_RTP_START:-10000}
SIP_RTP_END: ${SIP_RTP_END:-10099}
diff --git a/frontend/src/features/sip/SipPhone.tsx b/frontend/src/features/sip/SipPhone.tsx
index 74e506b..4e68a1d 100644
--- a/frontend/src/features/sip/SipPhone.tsx
+++ b/frontend/src/features/sip/SipPhone.tsx
@@ -12,12 +12,19 @@ const LABEL: Record = {
error: "ошибка",
};
+/** Абонент быстрого набора: пульт ДДС звонит бригаде и начальнику одной кнопкой. */
+export interface SipContact {
+ label: string;
+ number: string;
+}
+
export function SipPhone({
- defaultExtension = "6101", defaultTarget = "6102", title = "Локальный SIP/WebRTC",
+ defaultExtension = "6101", defaultTarget = "6102", title = "Локальный SIP/WebRTC", contacts = [],
}: {
defaultExtension?: string;
defaultTarget?: string;
title?: string;
+ contacts?: SipContact[];
}) {
const phone = useSipPhone();
const [extension, setExtension] = useState(defaultExtension);
@@ -46,6 +53,10 @@ export function SipPhone({
{phone.status === "registered" && }
+ {phone.status === "registered" && contacts.map((contact) => )}
{phone.status === "incoming" && }
{inCall && phone.status !== "incoming" && {phone.peer || target}}
{inCall && }
diff --git a/frontend/src/pages/dds/Dds.tsx b/frontend/src/pages/dds/Dds.tsx
index 59267b6..fbe6e47 100644
--- a/frontend/src/pages/dds/Dds.tsx
+++ b/frontend/src/pages/dds/Dds.tsx
@@ -604,9 +604,11 @@ export function Dds() {
}>
-
+
-
На втором рабочем месте откройте телефон старшего группы и зарегистрируйте 6102. Пароли 6101/6102 выдаёт локальный администратор командой make sip-credentials.
+
На втором рабочем месте откройте телефон старшего группы (6102); преподаватель в роли вышестоящего начальника — телефон 6103. Пароли 6101–6103 выдаёт локальный администратор командой make sip-credentials.
Учебный протокол доклада
Связь используется при необходимости. Разговор и учебный доклад сохраняются в журнале, но не блокируют изменение статуса.
diff --git a/frontend/src/pages/phone/Phone.tsx b/frontend/src/pages/phone/Phone.tsx
index bf767b0..cff22a4 100644
--- a/frontend/src/pages/phone/Phone.tsx
+++ b/frontend/src/pages/phone/Phone.tsx
@@ -1,9 +1,22 @@
import { SipPhone } from "@/features/sip/SipPhone";
+// Второе рабочее место голосовой связи ДДС. Старший группы — по умолчанию;
+// вышестоящего начальника (?role=superior) играет преподаватель: это
+// обязательный маршрут по ответу заказчика (П.7).
+const ROLES = {
+ crew: { title: "Телефон старшего группы", extension: "6102", heading: "IP-телефон старшего группы" },
+ superior: { title: "Телефон вышестоящего начальника", extension: "6103", heading: "IP-телефон вышестоящего начальника" },
+} as const;
+
export function Phone() {
+ const role = new URLSearchParams(window.location.search).get("role") === "superior" ? "superior" : "crew";
+ const { title, extension, heading } = ROLES[role];
return
-
IP-телефон старшего группы
-
Откройте этот экран на втором рабочем месте, зарегистрируйте номер 6102 и примите вызов ДДС 6101.
-
+
{heading}
+
Откройте этот экран на втором рабочем месте, зарегистрируйте номер {extension} и примите вызов ДДС 6101.
+
+
{role === "crew"
+ ? <>Роль вышестоящего начальника — телефон 6103.>
+ : <>Роль старшего группы — телефон 6102.>}
;
}
diff --git a/sip/entrypoint.sh b/sip/entrypoint.sh
index b5bcfca..32ab598 100644
--- a/sip/entrypoint.sh
+++ b/sip/entrypoint.sh
@@ -10,6 +10,7 @@ override_6002=${SIP_6002_PASSWORD:-}
override_6003=${SIP_6003_PASSWORD:-}
override_6101=${SIP_6101_PASSWORD:-}
override_6102=${SIP_6102_PASSWORD:-}
+override_6103=${SIP_6103_PASSWORD:-}
if [ -s "$credentials" ]; then
# Файл содержит только сгенерированные hex-значения без shell-конструкций.
. "$credentials"
@@ -19,6 +20,7 @@ SIP_6002_PASSWORD=${override_6002:-${SIP_6002_PASSWORD:-$(openssl rand -hex 16)}
SIP_6003_PASSWORD=${override_6003:-${SIP_6003_PASSWORD:-$(openssl rand -hex 16)}}
SIP_6101_PASSWORD=${override_6101:-${SIP_6101_PASSWORD:-$(openssl rand -hex 16)}}
SIP_6102_PASSWORD=${override_6102:-${SIP_6102_PASSWORD:-$(openssl rand -hex 16)}}
+SIP_6103_PASSWORD=${override_6103:-${SIP_6103_PASSWORD:-$(openssl rand -hex 16)}}
SIP_EXTERNAL_MEDIA_ADDRESS=${SIP_EXTERNAL_MEDIA_ADDRESS:-127.0.0.1}
SIP_RTP_START=${SIP_RTP_START:-10000}
SIP_RTP_END=${SIP_RTP_END:-10099}
@@ -36,12 +38,13 @@ SIP_6002_PASSWORD=$SIP_6002_PASSWORD
SIP_6003_PASSWORD=$SIP_6003_PASSWORD
SIP_6101_PASSWORD=$SIP_6101_PASSWORD
SIP_6102_PASSWORD=$SIP_6102_PASSWORD
+SIP_6103_PASSWORD=$SIP_6103_PASSWORD
EOF
chmod 600 "$credentials"
-export SIP_6001_PASSWORD SIP_6002_PASSWORD SIP_6003_PASSWORD SIP_6101_PASSWORD SIP_6102_PASSWORD SIP_EXTERNAL_MEDIA_ADDRESS
+export SIP_6001_PASSWORD SIP_6002_PASSWORD SIP_6003_PASSWORD SIP_6101_PASSWORD SIP_6102_PASSWORD SIP_6103_PASSWORD SIP_EXTERNAL_MEDIA_ADDRESS
export SIP_RTP_START SIP_RTP_END
-envsubst '${SIP_6001_PASSWORD} ${SIP_6002_PASSWORD} ${SIP_6003_PASSWORD} ${SIP_6101_PASSWORD} ${SIP_6102_PASSWORD} ${SIP_EXTERNAL_MEDIA_ADDRESS}' \
+envsubst '${SIP_6001_PASSWORD} ${SIP_6002_PASSWORD} ${SIP_6003_PASSWORD} ${SIP_6101_PASSWORD} ${SIP_6102_PASSWORD} ${SIP_6103_PASSWORD} ${SIP_EXTERNAL_MEDIA_ADDRESS}' \
< /opt/lct-sip/pjsip.conf.template > /etc/asterisk/pjsip.conf
chmod 640 /etc/asterisk/pjsip.conf
chown root:asterisk /etc/asterisk/pjsip.conf
diff --git a/sip/extensions.conf b/sip/extensions.conf
index 1d68ff5..0c2373c 100644
--- a/sip/extensions.conf
+++ b/sip/extensions.conf
@@ -12,9 +12,10 @@ exten => _600[1-3],1,NoOp(LCT training call ${CALLERID(num)} -> ${EXTEN})
same => n,StopMixMonitor()
same => n,Hangup()
-; 6101/6102 — браузерные WebRTC-абоненты. Тот же учебный контекст и запись,
-; но отдельные endpoints не ломают обычные UDP/TCP-софтфоны 6001–6003.
-exten => _610[1-2],1,NoOp(LCT WebRTC training call ${CALLERID(num)} -> ${EXTEN})
+; 6101–6103 — браузерные WebRTC-абоненты: ДДС, старший группы, вышестоящий
+; начальник. Тот же учебный контекст и запись, но отдельные endpoints не ломают
+; обычные UDP/TCP-софтфоны 6001–6003.
+exten => _610[1-3],1,NoOp(LCT WebRTC training call ${CALLERID(num)} -> ${EXTEN})
same => n,Set(RECORDING_FILE=${STRFTIME(${EPOCH},UTC,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN}-${UNIQUEID})
same => n,MixMonitor(/recordings/${RECORDING_FILE}.wav,b)
same => n,Dial(PJSIP/${EXTEN},30)
diff --git a/sip/pjsip.conf.template b/sip/pjsip.conf.template
index b75f5f0..6bd35df 100644
--- a/sip/pjsip.conf.template
+++ b/sip/pjsip.conf.template
@@ -157,3 +157,22 @@ type=aor
max_contacts=2
remove_existing=yes
qualify_frequency=0
+
+; Вышестоящий начальник ДДС: роль играет преподаватель со страницы
+; /phone?role=superior. Обязательный голосовой маршрут по ответу заказчика (П.7).
+[6103](webrtc-endpoint)
+auth=6103-auth
+aors=6103
+callerid=Веб вышестоящий начальник <6103>
+
+[6103-auth]
+type=auth
+auth_type=userpass
+username=6103
+password=${SIP_6103_PASSWORD}
+
+[6103]
+type=aor
+max_contacts=2
+remove_existing=yes
+qualify_frequency=0