feat(ui): зелёная кнопка ответа и автообновление PWA

This commit is contained in:
AidarKC 2026-04-22 19:49:32 +03:00
parent e0333a9c32
commit 1a8d1c70fd
5 changed files with 200 additions and 2 deletions

View File

@ -0,0 +1,75 @@
# Установка TURN сервера (coturn)
## 1. Что нужно
- Сервер с публичным IP (в примере: `37.214.58.208`).
- Доступ `root` по SSH.
- Открытые порты в firewall:
- `3478/tcp`
- `3478/udp`
- диапазон relay-портов, например `49160-49200/udp`
## 2. Быстрая установка (рекомендуется)
В проекте уже есть готовый скрипт:
```bash
sudo bash scripts/setup_turn_coturn.sh \
--secret "CHANGE_ME_LONG_RANDOM_SECRET" \
--realm "shineup.me"
```
Что делает скрипт:
- ставит `coturn`;
- включает режим `use-auth-secret` (временные логин/пароль);
- пишет `/etc/turnserver.conf`;
- включает и перезапускает сервис `coturn`.
## 3. Проверка
Проверить статус:
```bash
sudo systemctl status coturn --no-pager
```
Проверить, что порт слушается:
```bash
sudo ss -lntup | grep 3478
```
## 4. Ручная установка (если без скрипта)
```bash
sudo apt-get update
sudo apt-get install -y coturn
```
Пример `/etc/turnserver.conf`:
```conf
listening-port=3478
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret=CHANGE_ME_LONG_RANDOM_SECRET
realm=shineup.me
external-ip=37.214.58.208
listening-ip=0.0.0.0
relay-ip=37.214.58.208
min-port=49160
max-port=49200
no-cli
simple-log
```
В `/etc/default/coturn`:
```conf
TURNSERVER_ENABLED=1
```
Запуск:
```bash
sudo systemctl enable coturn
sudo systemctl restart coturn
```

View File

@ -0,0 +1,48 @@
# Подключение TURN к SHiNE-серверу
Начиная с текущей версии, клиент звонков запрашивает ICE-конфиг у backend через WS-операцию `GetCallIceConfig` и использует её для `RTCPeerConnection`.
## 1. Настройки backend
Файл: `src/main/resources/application.properties`
Ключи:
```properties
call.ice.stun.urls=stun:stun.l.google.com:19302
call.ice.turn.urls=turn:37.214.58.208:3478?transport=udp,turn:37.214.58.208:3478?transport=tcp
call.ice.turn.ttlSec=600
call.ice.turn.userPrefix=shine
call.ice.turn.sharedSecret=CHANGE_ME_LONG_RANDOM_SECRET
# fallback (если не используете shared-secret)
call.ice.turn.username=
call.ice.turn.password=
```
## 2. Рекомендуемый режим (временные credentials)
- На coturn и на SHiNE-сервере должен быть **одинаковый** secret:
- coturn: `static-auth-secret=...`
- SHiNE: `call.ice.turn.sharedSecret=...`
- Тогда SHiNE выдаёт короткоживущие `turnUsername/turnPassword` (TTL).
## 3. Fallback режим (статический логин/пароль)
Если временные credentials не используются:
```properties
call.ice.turn.sharedSecret=
call.ice.turn.username=turn_user
call.ice.turn.password=turn_password
```
## 4. Деплой после изменения
```bash
./gradlew deployServerNoCleanNoTests
./gradlew deployWEB
```
## 5. Проверка
1. Авторизоваться двумя клиентами.
2. Запустить звонок.
3. Проверить, что звонок устанавливается даже в сети, где прямой P2P затруднён.
4. Если TURN недоступен, клиент автоматически откатится к STUN-конфигу по умолчанию.

View File

@ -46,7 +46,7 @@ function ensureUi() {
acceptBtn = document.createElement('button');
acceptBtn.type = 'button';
acceptBtn.className = 'primary-btn';
acceptBtn.className = 'call-accept-btn';
acceptBtn.textContent = 'Ответить';
acceptBtn.addEventListener('click', async () => {
await acceptIncomingCall();

View File

@ -1,4 +1,49 @@
const LS_KEY = 'shine-ui-webpush-subscription-v1';
const SW_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
let swUpdateIntervalId = null;
let controllerChangeHandled = false;
function setupServiceWorkerAutoUpdate(registration, log) {
if (!registration) return;
if (!controllerChangeHandled && 'serviceWorker' in navigator) {
controllerChangeHandled = true;
navigator.serviceWorker.addEventListener('controllerchange', () => {
log({
level: 'info',
source: 'web-push',
message: 'Service Worker обновился, перезагружаем UI',
});
window.location.reload();
});
}
registration.addEventListener('updatefound', () => {
const installing = registration.installing;
if (!installing) return;
log({
level: 'info',
source: 'web-push',
message: 'Найдено обновление Service Worker',
});
});
if (swUpdateIntervalId) {
window.clearInterval(swUpdateIntervalId);
swUpdateIntervalId = null;
}
const checkNow = () => {
registration.update().catch(() => {});
};
checkNow();
swUpdateIntervalId = window.setInterval(checkNow, SW_UPDATE_CHECK_INTERVAL_MS);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible') return;
checkNow();
});
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
@ -36,13 +81,16 @@ export async function initPwaPush({ authService, onLog = null }) {
}
try {
const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js');
const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js', {
updateViaCache: 'none',
});
log({
level: 'info',
source: 'web-push',
message: 'Service Worker зарегистрирован',
details: { scope: registration.scope },
});
setupServiceWorkerAutoUpdate(registration, log);
const permission = await Notification.requestPermission();
if (permission !== 'granted') {

View File

@ -868,6 +868,33 @@
gap: 8px;
}
.call-accept-btn {
border: 1px solid rgba(122, 230, 166, 0.64);
border-radius: var(--radius-sm);
background: linear-gradient(120deg, rgba(98, 210, 146, 0.95), rgba(61, 164, 108, 0.94));
color: #052113;
padding: 9px 12px;
min-height: 38px;
font-weight: 700;
cursor: pointer;
transition: 0.2s ease;
}
.call-accept-btn:hover {
border-color: rgba(156, 246, 194, 0.88);
transform: translateY(-1px);
}
.call-accept-btn:disabled {
opacity: 1;
color: #93a6cc;
background: linear-gradient(180deg, rgba(18, 30, 54, 0.8), rgba(11, 20, 39, 0.86));
border-color: rgba(124, 145, 189, 0.28);
box-shadow: none;
cursor: not-allowed;
transform: none;
}
.pwa-diag-list {
gap: 8px;
}