diff --git a/VERSION.properties b/VERSION.properties index 5a0de82..46cc002 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.24 -server.version=1.2.24 +client.version=1.2.25 +server.version=1.2.25 diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 5d0f541..e6ed53a 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -43,6 +43,7 @@ import * as profileView from './pages/profile-view.js'; import * as profileEditView from './pages/profile-edit-view.js'; import * as walletView from './pages/wallet-view.js'; import * as settingsView from './pages/settings-view.js'; +import * as developerSettingsView from './pages/developer-settings-view.js'; import * as serverSettingsView from './pages/server-settings-view.js'; import * as deviceView from './pages/device-view.js'; import * as connectDeviceView from './pages/connect-device-view.js'; @@ -79,6 +80,7 @@ const routes = { 'profile-edit-view': profileEditView, 'wallet-view': walletView, 'settings-view': settingsView, + 'developer-settings-view': developerSettingsView, 'server-settings-view': serverSettingsView, 'device-view': deviceView, 'connect-device-view': connectDeviceView, diff --git a/shine-UI/js/pages/app-log-view.js b/shine-UI/js/pages/app-log-view.js index be3b765..acc5f16 100644 --- a/shine-UI/js/pages/app-log-view.js +++ b/shine-UI/js/pages/app-log-view.js @@ -18,7 +18,7 @@ export function render({ navigate }) { screen.append( renderHeader({ title: 'Лог приложения', - leftAction: { label: '←', onClick: () => navigate('settings-view') }, + leftAction: { label: '←', onClick: () => navigate('developer-settings-view') }, }), ); diff --git a/shine-UI/js/pages/developer-settings-view.js b/shine-UI/js/pages/developer-settings-view.js new file mode 100644 index 0000000..82995d3 --- /dev/null +++ b/shine-UI/js/pages/developer-settings-view.js @@ -0,0 +1,324 @@ +import { renderHeader } from '../components/header.js'; +import { addAppLogEntry, authService, state } from '../state.js'; +import { + canInstallPwa, + isStandalonePwaMode, + onPwaInstallAvailabilityChange, + promptPwaInstall, +} from '../services/pwa-install-service.js'; +import { initPwaPush } from '../services/pwa-push-service.js'; +import { getArweaveWalletFromStoredDeviceKey } from '../services/arweave-wallet-service.js'; +import { + prepareAvatarImageFile, + uploadArweaveFile, + validateAvatarSourceFile, +} from '../services/arweave-file-service.js'; + +export const pageMeta = { id: 'developer-settings-view', title: 'Настройки разработчика' }; + +function clearArweaveJwk(walletCtx) { + if (!walletCtx?.jwk || typeof walletCtx.jwk !== 'object') return; + Object.keys(walletCtx.jwk).forEach((key) => { + walletCtx.jwk[key] = ''; + }); + walletCtx.jwk = null; +} + +function formatBytes(bytes) { + const value = Number(bytes || 0); + if (!Number.isFinite(value) || value <= 0) return '0 B'; + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / (1024 * 1024)).toFixed(2)} MB`; +} + +function openDeveloperAvatarUploadModal({ walletLogin, storagePwd, gateway } = {}) { + const root = document.getElementById('modal-root'); + if (!root) return; + + root.innerHTML = ` + + `; + + const modal = root.querySelector('#settings-dev-avatar-modal'); + const loginInput = root.querySelector('#settings-dev-avatar-login'); + const fileInput = root.querySelector('#settings-dev-avatar-file'); + const metaEl = root.querySelector('#settings-dev-avatar-meta'); + const errorEl = root.querySelector('#settings-dev-avatar-error'); + const cancelBtn = root.querySelector('#settings-dev-avatar-cancel'); + const uploadBtn = root.querySelector('#settings-dev-avatar-upload'); + const resultCard = root.querySelector('#settings-dev-avatar-result'); + const txidEl = root.querySelector('#settings-dev-avatar-txid'); + const copyBtn = root.querySelector('#settings-dev-avatar-copy'); + + if (loginInput instanceof HTMLInputElement) { + loginInput.value = String(walletLogin || '').trim(); + } + + let isClosed = false; + const closeModal = () => { + if (isClosed) return; + isClosed = true; + root.innerHTML = ''; + }; + + const setError = (text) => { + if (errorEl) errorEl.textContent = String(text || ''); + }; + const setMeta = (text) => { + if (metaEl) metaEl.textContent = String(text || ''); + }; + const setTxId = (txId) => { + if (!(resultCard instanceof HTMLElement) || !(txidEl instanceof HTMLElement)) return; + const value = String(txId || '').trim(); + txidEl.textContent = value; + resultCard.hidden = !value; + }; + + modal?.addEventListener('click', (event) => { + if (event.target === modal) closeModal(); + }); + cancelBtn?.addEventListener('click', closeModal); + copyBtn?.addEventListener('click', async () => { + const txId = String(txidEl?.textContent || '').trim(); + if (!txId) return; + try { + await navigator.clipboard.writeText(txId); + setError(''); + } catch { + setError('Не удалось скопировать TX ID.'); + } + }); + + uploadBtn?.addEventListener('click', async () => { + setError(''); + setTxId(''); + const targetLogin = String(loginInput?.value || '').trim(); + const file = fileInput?.files?.[0] || null; + if (!targetLogin) { + setError('Введите логин пользователя.'); + return; + } + if (!file) { + setError('Выберите файл изображения.'); + return; + } + if (!String(walletLogin || '').trim() || !String(storagePwd || '').trim()) { + setError('Нет активной сессии. Войдите заново и повторите.'); + return; + } + + uploadBtn.disabled = true; + let walletCtx = null; + try { + validateAvatarSourceFile(file); + setMeta('Подготовка изображения...'); + const optimized = await prepareAvatarImageFile(file); + setMeta( + `Файл подготовлен: ${formatBytes(optimized.originalSizeBytes)} → ${formatBytes(optimized.optimizedSizeBytes)} ` + + `(${optimized.width}x${optimized.height}, ${optimized.contentType})`, + ); + + walletCtx = await getArweaveWalletFromStoredDeviceKey({ + login: walletLogin, + storagePwd, + onStatus: (message) => setMeta(message), + }); + setMeta('Загрузка в Arweave...'); + + const uploaded = await uploadArweaveFile({ + gateway, + jwk: walletCtx?.jwk, + file: optimized.file, + tags: [ + { name: 'SHiNE-Profile-Login', value: targetLogin }, + ], + }); + const txId = String(uploaded?.id || '').trim(); + if (!txId) throw new Error('Пустой Transaction ID'); + + setMeta('Загрузка завершена.'); + setTxId(txId); + } catch (error) { + const message = String(error?.message || ''); + if ( + message === 'Выберите файл изображения.' + || message === 'Поддерживаются только JPEG, PNG или WebP.' + || message === 'Файл слишком большой. Максимум 10 MB.' + || message === 'Не удалось подготовить изображение.' + ) { + setError(message); + } else { + setError('Не удалось загрузить аватар в Arweave.'); + } + } finally { + clearArweaveJwk(walletCtx); + uploadBtn.disabled = false; + } + }); +} + +async function forceUiUpdateNow() { + if (!('serviceWorker' in navigator)) { + window.location.reload(); + return; + } + try { + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map(async (registration) => { + try { await registration.update(); } catch {} + if (registration.waiting) { + try { registration.waiting.postMessage({ type: 'SKIP_WAITING' }); } catch {} + } + })); + } catch {} + window.setTimeout(() => window.location.reload(), 450); +} + +function showClientUpdateHelp() { + window.alert( + 'Если UI не обновился:\n\n' + + '1) Закройте вкладки с SHiNE.\n' + + '2) Откройте chrome://settings/siteData и удалите данные для shineup.me.\n' + + '3) Если приложение установлено как PWA — удалите его с устройства.\n' + + '4) Откройте https://shineup.me заново и выполните вход.\n' + + '5) Если всё ещё старая версия — откройте в режиме инкогнито и проверьте версию в Настройки -> Версии.' + ); +} + +export function render({ navigate }) { + const screen = document.createElement('section'); + screen.className = 'stack'; + + screen.append( + renderHeader({ + title: 'Настройки разработчика', + leftAction: { label: '←', onClick: () => navigate('settings-view') }, + }), + ); + + const card = document.createElement('div'); + card.className = 'card stack settings-developer-card'; + card.innerHTML = ` + + + + + + + `; + + const appLogBtn = card.querySelector('#settings-app-log'); + const diagnosticsBtn = card.querySelector('#settings-pwa-diagnostics'); + const pwaInstallBtn = card.querySelector('#settings-pwa-install'); + const uploadAvatarBtn = card.querySelector('#settings-upload-avatar'); + const forceUpdateBtn = card.querySelector('#settings-force-ui-update'); + const forceUpdateHelpBtn = card.querySelector('#settings-force-update-help'); + + appLogBtn?.addEventListener('click', () => navigate('app-log-view')); + diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view')); + uploadAvatarBtn?.addEventListener('click', () => { + openDeveloperAvatarUploadModal({ + walletLogin: state.session.login, + storagePwd: state.session.storagePwdInMemory, + gateway: state.entrySettings.arweaveServer, + }); + }); + forceUpdateHelpBtn?.addEventListener('click', showClientUpdateHelp); + + forceUpdateBtn?.addEventListener('click', async () => { + forceUpdateBtn.disabled = true; + try { + addAppLogEntry({ + level: 'info', + source: 'ui-update', + message: 'Пользователь запросил принудительное обновление UI', + }); + await forceUiUpdateNow(); + } finally { + forceUpdateBtn.disabled = false; + } + }); + + const syncPwaButtonLabel = () => { + if (isStandalonePwaMode()) { + pwaInstallBtn.textContent = 'PWA установлено (проверить WebPush)'; + return; + } + if (canInstallPwa()) { + pwaInstallBtn.textContent = 'Зарегистрировать PWA'; + return; + } + pwaInstallBtn.textContent = 'Как установить PWA'; + }; + + const unsubscribeInstallAvailability = onPwaInstallAvailabilityChange(() => { + syncPwaButtonLabel(); + }); + syncPwaButtonLabel(); + + pwaInstallBtn.addEventListener('click', async () => { + pwaInstallBtn.disabled = true; + try { + await initPwaPush({ + authService, + onLog: (entry) => addAppLogEntry(entry), + }); + + if (canInstallPwa()) { + const result = await promptPwaInstall(); + const accepted = result.outcome === 'accepted'; + addAppLogEntry({ + level: 'info', + source: 'pwa-install', + message: accepted ? 'Пользователь принял установку PWA' : 'Пользователь отклонил установку PWA', + details: { outcome: result.outcome || 'unknown' }, + }); + if (accepted) { + window.alert('Установка PWA подтверждена. Проверьте приложение на главном экране устройства.'); + } + } else if (!isStandalonePwaMode()) { + window.alert('Для установки откройте меню браузера и выберите "Установить приложение" или "Добавить на главный экран".'); + } else { + window.alert('PWA уже установлено. WebPush перерегистрирован.'); + } + } catch (error) { + addAppLogEntry({ + level: 'warn', + source: 'pwa-install', + message: 'Не удалось зарегистрировать PWA/WebPush', + details: { error: error?.message || 'unknown' }, + }); + window.alert(`Ошибка регистрации PWA: ${error?.message || 'unknown'}`); + } finally { + pwaInstallBtn.disabled = false; + syncPwaButtonLabel(); + } + }); + + screen.cleanup = () => { + unsubscribeInstallAvailability(); + }; + screen.append(card); + return screen; +} diff --git a/shine-UI/js/pages/pwa-diagnostics-view.js b/shine-UI/js/pages/pwa-diagnostics-view.js index 161bf54..f9312e6 100644 --- a/shine-UI/js/pages/pwa-diagnostics-view.js +++ b/shine-UI/js/pages/pwa-diagnostics-view.js @@ -272,7 +272,7 @@ export function render({ navigate }) { screen.append( renderHeader({ title: 'Диагностика PWA / Push', - leftAction: { label: '←', onClick: () => navigate('settings-view') }, + leftAction: { label: '←', onClick: () => navigate('developer-settings-view') }, }), ); diff --git a/shine-UI/js/pages/settings-view.js b/shine-UI/js/pages/settings-view.js index 260b759..8a50d52 100644 --- a/shine-UI/js/pages/settings-view.js +++ b/shine-UI/js/pages/settings-view.js @@ -1,18 +1,5 @@ import { renderHeader } from '../components/header.js'; -import { addAppLogEntry, authService, closeCurrentSessionAndSignOut, state } from '../state.js'; -import { - canInstallPwa, - isStandalonePwaMode, - onPwaInstallAvailabilityChange, - promptPwaInstall, -} from '../services/pwa-install-service.js'; -import { initPwaPush } from '../services/pwa-push-service.js'; -import { getArweaveWalletFromStoredDeviceKey } from '../services/arweave-wallet-service.js'; -import { - prepareAvatarImageFile, - uploadArweaveFile, - validateAvatarSourceFile, -} from '../services/arweave-file-service.js'; +import { addAppLogEntry, authService, closeCurrentSessionAndSignOut } from '../state.js'; export const pageMeta = { id: 'settings-view', title: 'Настройки' }; @@ -38,168 +25,6 @@ function formatVersionForUi(rawValue) { return value; } -function clearArweaveJwk(walletCtx) { - if (!walletCtx?.jwk || typeof walletCtx.jwk !== 'object') return; - Object.keys(walletCtx.jwk).forEach((key) => { - walletCtx.jwk[key] = ''; - }); - walletCtx.jwk = null; -} - -function formatBytes(bytes) { - const value = Number(bytes || 0); - if (!Number.isFinite(value) || value <= 0) return '0 B'; - if (value < 1024) return `${value} B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; - return `${(value / (1024 * 1024)).toFixed(2)} MB`; -} - -function openDeveloperAvatarUploadModal({ walletLogin, storagePwd, gateway } = {}) { - const root = document.getElementById('modal-root'); - if (!root) return; - - root.innerHTML = ` - - `; - - const modal = root.querySelector('#settings-dev-avatar-modal'); - const loginInput = root.querySelector('#settings-dev-avatar-login'); - const fileInput = root.querySelector('#settings-dev-avatar-file'); - const metaEl = root.querySelector('#settings-dev-avatar-meta'); - const errorEl = root.querySelector('#settings-dev-avatar-error'); - const cancelBtn = root.querySelector('#settings-dev-avatar-cancel'); - const uploadBtn = root.querySelector('#settings-dev-avatar-upload'); - const resultCard = root.querySelector('#settings-dev-avatar-result'); - const txidEl = root.querySelector('#settings-dev-avatar-txid'); - const copyBtn = root.querySelector('#settings-dev-avatar-copy'); - - if (loginInput instanceof HTMLInputElement) { - loginInput.value = String(walletLogin || '').trim(); - } - - let isClosed = false; - const closeModal = () => { - if (isClosed) return; - isClosed = true; - root.innerHTML = ''; - }; - - const setError = (text) => { - if (errorEl) errorEl.textContent = String(text || ''); - }; - const setMeta = (text) => { - if (metaEl) metaEl.textContent = String(text || ''); - }; - const setTxId = (txId) => { - if (!(resultCard instanceof HTMLElement) || !(txidEl instanceof HTMLElement)) return; - const value = String(txId || '').trim(); - txidEl.textContent = value; - resultCard.hidden = !value; - }; - - modal?.addEventListener('click', (event) => { - if (event.target === modal) closeModal(); - }); - cancelBtn?.addEventListener('click', closeModal); - copyBtn?.addEventListener('click', async () => { - const txId = String(txidEl?.textContent || '').trim(); - if (!txId) return; - try { - await navigator.clipboard.writeText(txId); - setError(''); - } catch { - setError('Не удалось скопировать TX ID.'); - } - }); - - uploadBtn?.addEventListener('click', async () => { - setError(''); - setTxId(''); - const targetLogin = String(loginInput?.value || '').trim(); - const file = fileInput?.files?.[0] || null; - if (!targetLogin) { - setError('Введите логин пользователя.'); - return; - } - if (!file) { - setError('Выберите файл изображения.'); - return; - } - if (!String(walletLogin || '').trim() || !String(storagePwd || '').trim()) { - setError('Нет активной сессии. Войдите заново и повторите.'); - return; - } - - uploadBtn.disabled = true; - let walletCtx = null; - try { - validateAvatarSourceFile(file); - setMeta('Подготовка изображения...'); - const optimized = await prepareAvatarImageFile(file); - setMeta( - `Файл подготовлен: ${formatBytes(optimized.originalSizeBytes)} → ${formatBytes(optimized.optimizedSizeBytes)} ` + - `(${optimized.width}x${optimized.height}, ${optimized.contentType})`, - ); - - walletCtx = await getArweaveWalletFromStoredDeviceKey({ - login: walletLogin, - storagePwd, - onStatus: (message) => setMeta(message), - }); - setMeta('Загрузка в Arweave...'); - - const uploaded = await uploadArweaveFile({ - gateway, - jwk: walletCtx?.jwk, - file: optimized.file, - tags: [ - { name: 'SHiNE-Profile-Login', value: targetLogin }, - ], - }); - const txId = String(uploaded?.id || '').trim(); - if (!txId) throw new Error('Пустой Transaction ID'); - - setMeta('Загрузка завершена.'); - setTxId(txId); - } catch (error) { - const message = String(error?.message || ''); - if ( - message === 'Выберите файл изображения.' - || message === 'Поддерживаются только JPEG, PNG или WebP.' - || message === 'Файл слишком большой. Максимум 10 MB.' - || message === 'Не удалось подготовить изображение.' - ) { - setError(message); - } else { - setError('Не удалось загрузить аватар в Arweave.'); - } - } finally { - clearArweaveJwk(walletCtx); - uploadBtn.disabled = false; - } - }); -} - export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; @@ -218,79 +43,16 @@ export function render({ navigate }) { - + `; card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view')); card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view')); card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view')); - card.querySelector('#settings-force-update-help').addEventListener('click', () => { - window.alert( - 'Если UI не обновился:\n\n' - + '1) Закройте вкладки с SHiNE.\n' - + '2) Откройте chrome://settings/siteData и удалите данные для shineup.me.\n' - + '3) Если приложение установлено как PWA — удалите его с устройства.\n' - + '4) Откройте https://shineup.me заново и выполните вход.\n' - + '5) Если всё ещё старая версия — откройте в режиме инкогнито и проверьте версию в Настройки -> Версии.' - ); - }); + card.querySelector('#settings-developer').addEventListener('click', () => navigate('developer-settings-view')); const signOutBtn = card.querySelector('#settings-signout'); - const developerCard = document.createElement('div'); - developerCard.className = 'card stack settings-developer-card'; - developerCard.innerHTML = ` - - - `; - - const developerToggleBtn = developerCard.querySelector('#settings-dev-toggle'); - const developerPanel = developerCard.querySelector('#settings-dev-panel'); - const appLogBtn = developerCard.querySelector('#settings-app-log'); - const diagnosticsBtn = developerCard.querySelector('#settings-pwa-diagnostics'); - const pwaInstallBtn = developerCard.querySelector('#settings-pwa-install'); - const uploadAvatarBtn = developerCard.querySelector('#settings-upload-avatar'); - - developerToggleBtn?.addEventListener('click', () => { - const isHidden = developerPanel?.hidden !== false; - if (developerPanel) developerPanel.hidden = !isHidden; - if (developerToggleBtn) { - developerToggleBtn.textContent = isHidden ? 'Для разработчиков ▲' : 'Для разработчиков ▼'; - } - }); - - appLogBtn?.addEventListener('click', () => navigate('app-log-view')); - diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view')); - uploadAvatarBtn?.addEventListener('click', () => { - openDeveloperAvatarUploadModal({ - walletLogin: state.session.login, - storagePwd: state.session.storagePwdInMemory, - gateway: state.entrySettings.arweaveServer, - }); - }); - - const syncPwaButtonLabel = () => { - if (isStandalonePwaMode()) { - pwaInstallBtn.textContent = 'PWA установлено (проверить WebPush)'; - return; - } - if (canInstallPwa()) { - pwaInstallBtn.textContent = 'Зарегистрировать PWA'; - return; - } - pwaInstallBtn.textContent = 'Как установить PWA'; - }; - - const unsubscribeInstallAvailability = onPwaInstallAvailabilityChange(() => { - syncPwaButtonLabel(); - }); - syncPwaButtonLabel(); - signOutBtn.addEventListener('click', async () => { const confirmed = window.confirm( 'Завершить текущую сессию на сервере, отключиться, очистить локальные данные и перейти на стартовый экран?' @@ -302,7 +64,7 @@ export function render({ navigate }) { addAppLogEntry({ level: 'info', source: 'session', - message: `Запрошено завершение текущей сессии: ${state.session.sessionId || 'unknown'}`, + message: 'Запрошено завершение текущей сессии', }); await closeCurrentSessionAndSignOut({ infoMessage: 'Сеанс завершён. Выполните вход заново.', @@ -312,45 +74,6 @@ export function render({ navigate }) { } }); - pwaInstallBtn.addEventListener('click', async () => { - pwaInstallBtn.disabled = true; - try { - await initPwaPush({ - authService, - onLog: (entry) => addAppLogEntry(entry), - }); - - if (canInstallPwa()) { - const result = await promptPwaInstall(); - const accepted = result.outcome === 'accepted'; - addAppLogEntry({ - level: 'info', - source: 'pwa-install', - message: accepted ? 'Пользователь принял установку PWA' : 'Пользователь отклонил установку PWA', - details: { outcome: result.outcome || 'unknown' }, - }); - if (accepted) { - window.alert('Установка PWA подтверждена. Проверьте приложение на главном экране устройства.'); - } - } else if (!isStandalonePwaMode()) { - window.alert('Для установки откройте меню браузера и выберите "Установить приложение" или "Добавить на главный экран".'); - } else { - window.alert('PWA уже установлено. WebPush перерегистрирован.'); - } - } catch (error) { - addAppLogEntry({ - level: 'warn', - source: 'pwa-install', - message: 'Не удалось зарегистрировать PWA/WebPush', - details: { error: error?.message || 'unknown' }, - }); - window.alert(`Ошибка регистрации PWA: ${error?.message || 'unknown'}`); - } finally { - pwaInstallBtn.disabled = false; - syncPwaButtonLabel(); - } - }); - const versionCard = document.createElement('div'); versionCard.className = 'card stack'; @@ -394,12 +117,10 @@ export function render({ navigate }) { } })(); - screen.append(card); screen.cleanup = () => { isDisposed = true; - unsubscribeInstallAvailability(); }; + screen.append(card); screen.append(versionCard); - screen.append(developerCard); return screen; } diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js index c43760f..9449a3e 100644 --- a/shine-UI/js/router.js +++ b/shine-UI/js/router.js @@ -100,6 +100,7 @@ export function resolveToolbarActive(pageId) { pageId === 'profile-edit-view' || pageId === 'wallet-view' || pageId === 'settings-view' || + pageId === 'developer-settings-view' || pageId === 'server-settings-view' || pageId === 'device-view' || pageId === 'connect-device-view' || diff --git a/shine-UI/js/services/call-service.js b/shine-UI/js/services/call-service.js index b2cad83..767cbc6 100644 --- a/shine-UI/js/services/call-service.js +++ b/shine-UI/js/services/call-service.js @@ -284,20 +284,106 @@ function buildCallFactsJson(call, extra = {}) { } } +function buildCallFactsLine(call, extra = {}) { + const pc = call?.pc || null; + const facts = { + callId: call?.callId || '', + peerLogin: call?.peerLogin || '', + remoteSessionId: call?.remoteSessionId || '', + direction: call?.direction || '', + phase: call?.phase || '', + statusText: call?.statusText || '', + startedAtMs: Number(call?.startedAtMs || 0), + startedAtIso: toIsoTs(call?.startedAtMs), + connectedAtMs: Number(call?.connectedAtMs || 0), + connectedAtIso: toIsoTs(call?.connectedAtMs), + routeLabel: call?.connectionRouteLabel || '', + routeDetails: call?.connectionRouteDetails || '', + pcConnectionState: pc?.connectionState || '', + pcIceConnectionState: pc?.iceConnectionState || '', + pcSignalingState: pc?.signalingState || '', + hasLocalStream: Boolean(call?.localStream), + localAudioTracksCount: call?.localStream?.getAudioTracks?.()?.length || 0, + ...extra, + }; + return Object.entries(facts) + .map(([k, v]) => `${k}=${String(v ?? '').replace(/,/g, ';')}`) + .join(', '); +} + +function getCallDiagnosticsContext(call) { + const pc = call?.pc || null; + const nav = typeof navigator !== 'undefined' ? navigator : null; + const conn = nav?.connection || nav?.mozConnection || nav?.webkitConnection || null; + const permissionsApiAvailable = typeof nav?.permissions?.query === 'function'; + const mediaDevicesAvailable = Boolean(nav?.mediaDevices?.getUserMedia); + const online = typeof nav?.onLine === 'boolean' ? nav.onLine : null; + const visibilityState = typeof document !== 'undefined' ? String(document.visibilityState || '') : ''; + const pageFocused = typeof document !== 'undefined' && typeof document.hasFocus === 'function' + ? Boolean(document.hasFocus()) + : false; + const localTracks = call?.localStream?.getTracks?.() || []; + const localAudioTracks = call?.localStream?.getAudioTracks?.() || []; + const enabledLocalAudioTracks = localAudioTracks.filter((track) => track?.enabled).length; + const transceiversCount = pc?.getTransceivers?.()?.length || 0; + const sendersCount = pc?.getSenders?.()?.length || 0; + const receiversCount = pc?.getReceivers?.()?.length || 0; + const iceGatheringState = pc?.iceGatheringState || ''; + const currentLocalDescType = pc?.localDescription?.type || ''; + const currentRemoteDescType = pc?.remoteDescription?.type || ''; + + return { + remoteSessionIdPresent: Boolean(call?.remoteSessionId), + callExistsInStore: calls.has(String(call?.callId || '')), + browserOnline: online === null ? '' : String(online), + documentVisibilityState: visibilityState, + pageFocused, + userAgent: typeof nav?.userAgent === 'string' ? nav.userAgent : '', + platform: typeof nav?.platform === 'string' ? nav.platform : '', + language: typeof nav?.language === 'string' ? nav.language : '', + permissionsApiAvailable, + mediaDevicesApiAvailable: mediaDevicesAvailable, + connectionType: String(conn?.type || ''), + effectiveConnectionType: String(conn?.effectiveType || ''), + networkRttMs: Number(conn?.rtt || 0), + networkDownlinkMbps: Number(conn?.downlink || 0), + saveData: conn?.saveData === true, + localTrackCount: localTracks.length, + localAudioTracksCount: localAudioTracks.length, + localAudioTracksEnabledCount: enabledLocalAudioTracks, + localAudioTrackLabels: localAudioTracks.map((t) => String(t?.label || '')).join('|'), + hasPeerConnection: Boolean(pc), + pcConnectionState: pc?.connectionState || '', + pcIceConnectionState: pc?.iceConnectionState || '', + pcIceGatheringState: iceGatheringState, + pcSignalingState: pc?.signalingState || '', + pcCanTrickleIceCandidates: pc?.canTrickleIceCandidates === null || pc?.canTrickleIceCandidates === undefined + ? '' + : String(pc?.canTrickleIceCandidates), + localDescriptionType: currentLocalDescType, + remoteDescriptionType: currentRemoteDescType, + pcTransceiversCount: transceiversCount, + pcSendersCount: sendersCount, + pcReceiversCount: receiversCount, + }; +} + async function sendCallDeliveryReport(call, eventType, eventCode, reason = '', extraFacts = {}) { if (!call || !authService || typeof authService.sendCallDeliveryReport !== 'function') return; try { - const valueJson = buildCallFactsJson(call, { + const diagnostics = getCallDiagnosticsContext(call); + const valueLine = buildCallFactsLine(call, { eventType: String(eventType || '').trim(), eventCode: String(eventCode || '').trim(), reason: String(reason || '').trim(), reportedAtMs: nowMs(), reportedAtIso: toIsoTs(nowMs()), + ...diagnostics, ...extraFacts, }); await authService.sendCallDeliveryReport({ type: String(eventType || '').trim(), - value: valueJson, + value: valueLine, }); } catch {} } @@ -569,13 +655,23 @@ async function finalizeCall(call, { const reasonText = debugReason || localReasonCode; if (String(localReasonCode || '') !== 'completed') { + const failureStage = call.phase || ''; + const failureContext = { + failureStage, + connectedBeforeFailure: Boolean(call.connectedAtMs), + }; if (call.direction === 'out') { - await sendCallDeliveryReport(call, 'outgoing_failed', `outgoing_${localReasonCode}`, reasonText); + await sendCallDeliveryReport(call, 'outgoing_failed', `outgoing_${localReasonCode}`, reasonText, failureContext); } else if (call.direction === 'in') { - await sendCallDeliveryReport(call, 'incoming_failed', `incoming_${localReasonCode}`, reasonText); + await sendCallDeliveryReport(call, 'incoming_failed', `incoming_${localReasonCode}`, reasonText, failureContext); + } + if (String(localReasonCode || '') === 'busy') { + await sendCallDeliveryReport(call, 'call_busy', 'call_busy', reasonText, failureContext); + } else if (String(localReasonCode || '') === 'declined') { + await sendCallDeliveryReport(call, 'call_declined', 'call_declined', reasonText, failureContext); } if (String(localReasonCode || '') === 'error') { - await sendCallDeliveryReport(call, 'unknown_error', 'call_unknown_error', reasonText); + await sendCallDeliveryReport(call, 'unknown_error', 'call_unknown_error', reasonText, failureContext); } } @@ -1081,7 +1177,12 @@ export async function handleIncomingCallSignal(evt) { } if (type === TYPES.DECLINE_BUSY) { - await finalizeCall(call, { localReasonCode: 'busy', debugReason: 'decline_or_busy' }); + const normalized = data.trim().toLowerCase(); + const isDeclined = normalized === 'decline' || normalized === 'declined'; + await finalizeCall(call, { + localReasonCode: isDeclined ? 'declined' : 'busy', + debugReason: isDeclined ? 'declined_by_remote' : 'busy_by_remote', + }); return; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java index 5fa4fb7..4d64058 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java @@ -36,11 +36,16 @@ public class Net_CallDeliveryReport_Handler implements JsonMessageHandler { long serverTs = System.currentTimeMillis(); String line = String.format( Locale.ROOT, - "%s | type=%s | login=%s | remote=%s | value=%s%n", + "%s | type=%s | op=%s | requestId=%s | login=%s | sessionId=%s | authenticated=%s | remote=%s | userAgent=%s | value=%s%n", Instant.ofEpochMilli(serverTs), clip(req.getType(), 80), + clip(req.getOp(), 80), + clip(req.getRequestId(), 120), clip(ctx != null ? ctx.getLogin() : "", 80), + clip(ctx != null ? ctx.getSessionId() : "", 160), + ctx != null && ctx.isAuthenticatedUser(), clip(remoteAddress(ctx), 200), + clip(userAgent(ctx), 300), clip(req.getValue(), 8000) ); @@ -88,6 +93,18 @@ public class Net_CallDeliveryReport_Handler implements JsonMessageHandler { return value == null ? "" : value.trim(); } + private static String userAgent(ConnectionContext ctx) { + if (ctx == null) return ""; + Session ws = ctx.getWsSession(); + if (ws == null) return ""; + try { + String value = ws.getUpgradeRequest() != null ? ws.getUpgradeRequest().getHeader("User-Agent") : ""; + return safe(value); + } catch (Exception ignored) { + return ""; + } + } + private static String clip(String value, int maxLen) { String cleaned = safe(value).replace('\n', ' ').replace('\r', ' '); if (cleaned.length() <= maxLen) return cleaned; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7143818..7609457 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,7 +12,7 @@ server.info.physicalRegion= server.info.description= server.info.origin= server.info.extraInfo= -server.ui.indexPath=/home/user/docker/caddyFile/sites/shine-UI/index.html +server.ui.indexPath=/home/player/SHiNE/shine-UI/index.html server.ui.buildHash= # Web Push (VAPID) @@ -37,6 +37,23 @@ call.ice.turn.sharedSecret= call.ice.turn.username= call.ice.turn.password= +# ------------------------------------------------------------ +# Несколько TURN-серверов (рекомендуемый режим) +# Каждый блок описывает один TURN-узел. Новые узлы добавляются по индексу. +# Приоритет авторизации на узел: sharedSecret -> статические username/password. +# ------------------------------------------------------------ +call.ice.turn.servers.1.id=vps-05 +call.ice.turn.servers.1.urls=turn:45.136.124.227:3478?transport=udp,turn:45.136.124.227:3478?transport=tcp +call.ice.turn.servers.1.sharedSecret=def6d444734d380d2f67a9d345b1debf985eaba0973c343e392c060d97c30106 +call.ice.turn.servers.1.username= +call.ice.turn.servers.1.password= + +call.ice.turn.servers.2.id=promo-node-93 +call.ice.turn.servers.2.urls=turn:93.170.12.154:3478?transport=udp,turn:93.170.12.154:3478?transport=tcp +call.ice.turn.servers.2.sharedSecret=def6d444734d380d2f67a9d345b1debf985eaba0973c343e392c060d97c30106 +call.ice.turn.servers.2.username= +call.ice.turn.servers.2.password= + # ------------------------------------------------------------ # Временные debug HTTP API для тестирования соединений # true - endpoint'ы /debug/ws/* включены (только при наличии .debug-token)