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 = `
+
+
+
Загрузить аватар (Arweave)
+
+
+
+
+
Поддерживаются JPEG, PNG, WebP. Перед загрузкой изображение будет оптимизировано.
+
+
+
+
+
+
+
+
Transaction ID:
+
+
+
+
+
+ `;
+
+ 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 = `
-
-
-
Загрузить аватар (Arweave)
-
-
-
-
-
Поддерживаются JPEG, PNG, WebP. Перед загрузкой изображение будет оптимизировано.
-
-
-
-
-
-
-
-
Transaction ID:
-
-
-
-
-
- `;
-
- 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)