diff --git a/build.gradle b/build.gradle index 0d8dc0c..ee7e4c6 100644 --- a/build.gradle +++ b/build.gradle @@ -182,10 +182,18 @@ tasks.register('deployServer', JavaExec) { dependsOn testClasses } -tasks.register('deployPWA', Exec) { +tasks.register('deployWEB', Exec) { group = "!!deployment" - description = "Deploy PWA via deploy_shine-PWA.sh" + description = "Deploy WEB via deploy_shine-PWA.sh" workingDir = rootDir commandLine 'bash', file('deploy_shine-PWA.sh').absolutePath } + +tasks.register('deployAll') { + group = "!!deployment" + description = "Deploy server and WEB" + + dependsOn tasks.named('deployServer') + dependsOn tasks.named('deployWEB') +} diff --git a/shine-UI/index.html b/shine-UI/index.html index 61d6613..af28128 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -5,9 +5,9 @@ Shine UI Demo - - - + + +
@@ -27,6 +27,6 @@ }; window.__SHINE_FIREBASE_VAPID_KEY__ = ''; - + diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 386fe8d..274d5ae 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -1,7 +1,7 @@ -import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260405171816'; -import { renderToolbar } from './components/toolbar.js?v=20260405171816'; -import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js?v=20260405171816'; -import { initPwaPush } from './services/pwa-push-service.js?v=20260405171816'; +import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260407105357'; +import { renderToolbar } from './components/toolbar.js?v=20260407105357'; +import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js?v=20260407105357'; +import { initPwaPush } from './services/pwa-push-service.js?v=20260407105357'; import { authService, authorizeSession, @@ -12,38 +12,38 @@ import { terminateCurrentSession, addIncomingMessage, setContacts, -} from './state.js?v=20260405171816'; +} from './state.js?v=20260407105357'; -import * as startView from './pages/start-view.js?v=20260405171816'; -import * as entrySettingsView from './pages/entry-settings-view.js?v=20260405171816'; -import * as registerView from './pages/register-view.js?v=20260405171816'; -import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260405171816'; -import * as registrationKeysView from './pages/registration-keys-view.js?v=20260405171816'; -import * as topupView from './pages/topup-view.js?v=20260405171816'; -import * as loginView from './pages/login-view.js?v=20260405171816'; -import * as loginCameraView from './pages/login-camera-view.js?v=20260405171816'; -import * as loginPasswordView from './pages/login-password-view.js?v=20260405171816'; -import * as keyStorageView from './pages/key-storage-view.js?v=20260405171816'; +import * as startView from './pages/start-view.js?v=20260407105357'; +import * as entrySettingsView from './pages/entry-settings-view.js?v=20260407105357'; +import * as registerView from './pages/register-view.js?v=20260407105357'; +import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260407105357'; +import * as registrationKeysView from './pages/registration-keys-view.js?v=20260407105357'; +import * as topupView from './pages/topup-view.js?v=20260407105357'; +import * as loginView from './pages/login-view.js?v=20260407105357'; +import * as loginCameraView from './pages/login-camera-view.js?v=20260407105357'; +import * as loginPasswordView from './pages/login-password-view.js?v=20260407105357'; +import * as keyStorageView from './pages/key-storage-view.js?v=20260407105357'; -import * as profileView from './pages/profile-view.js?v=20260405171816'; -import * as walletView from './pages/wallet-view.js?v=20260405171816'; -import * as settingsView from './pages/settings-view.js?v=20260405171816'; -import * as serverSettingsView from './pages/server-settings-view.js?v=20260405171816'; -import * as deviceView from './pages/device-view.js?v=20260405171816'; -import * as connectDeviceView from './pages/connect-device-view.js?v=20260405171816'; -import * as deviceQrView from './pages/device-qr-view.js?v=20260405171816'; -import * as deviceCameraView from './pages/device-camera-view.js?v=20260405171816'; -import * as showKeysView from './pages/show-keys-view.js?v=20260405171816'; -import * as deviceSessionView from './pages/device-session-view.js?v=20260405171816'; -import * as languageView from './pages/language-view.js?v=20260405171816'; -import * as messagesList from './pages/messages-list.js?v=20260405171816'; -import * as contactSearchView from './pages/contact-search-view.js?v=20260405171816'; -import * as chatView from './pages/chat-view.js?v=20260405171816'; -import * as channelsList from './pages/channels-list.js?v=20260405171816'; -import * as channelView from './pages/channel-view.js?v=20260405171816'; -import * as addChannelView from './pages/add-channel-view.js?v=20260405171816'; -import * as networkView from './pages/network-view.js?v=20260405171816'; -import * as notificationsView from './pages/notifications-view.js?v=20260405171816'; +import * as profileView from './pages/profile-view.js?v=20260407105357'; +import * as walletView from './pages/wallet-view.js?v=20260407105357'; +import * as settingsView from './pages/settings-view.js?v=20260407105357'; +import * as serverSettingsView from './pages/server-settings-view.js?v=20260407105357'; +import * as deviceView from './pages/device-view.js?v=20260407105357'; +import * as connectDeviceView from './pages/connect-device-view.js?v=20260407105357'; +import * as deviceQrView from './pages/device-qr-view.js?v=20260407105357'; +import * as deviceCameraView from './pages/device-camera-view.js?v=20260407105357'; +import * as showKeysView from './pages/show-keys-view.js?v=20260407105357'; +import * as deviceSessionView from './pages/device-session-view.js?v=20260407105357'; +import * as languageView from './pages/language-view.js?v=20260407105357'; +import * as messagesList from './pages/messages-list.js?v=20260407105357'; +import * as contactSearchView from './pages/contact-search-view.js?v=20260407105357'; +import * as chatView from './pages/chat-view.js?v=20260407105357'; +import * as channelsList from './pages/channels-list.js?v=20260407105357'; +import * as channelView from './pages/channel-view.js?v=20260407105357'; +import * as addChannelView from './pages/add-channel-view.js?v=20260407105357'; +import * as networkView from './pages/network-view.js?v=20260407105357'; +import * as notificationsView from './pages/notifications-view.js?v=20260407105357'; const routes = { 'start-view': startView, @@ -84,7 +84,20 @@ let currentCleanup = null; setClientErrorTransport((payload) => authService.reportClientError(payload)); +function showGlobalErrorAlert(title, details = {}) { + const lines = [title]; + if (details.message) lines.push(`Сообщение: ${details.message}`); + if (details.pageId) lines.push(`Экран: ${details.pageId}`); + if (details.sourceUrl) lines.push(`Источник: ${details.sourceUrl}`); + if (Number.isFinite(details.lineNumber)) lines.push(`Строка: ${details.lineNumber}`); + if (Number.isFinite(details.columnNumber)) lines.push(`Колонка: ${details.columnNumber}`); + if (details.reasonType) lines.push(`Тип: ${details.reasonType}`); + if (details.stack) lines.push(`Stack:\n${details.stack}`); + window.alert(lines.join('\n')); +} + window.addEventListener('error', (event) => { + const pageId = getRoute().pageId || ''; captureClientError({ kind: 'global_error', message: event.message || 'Global JS error', @@ -93,22 +106,39 @@ window.addEventListener('error', (event) => { lineNumber: event.lineno, columnNumber: event.colno, context: { - pageId: getRoute().pageId || '', + pageId, }, }); + + showGlobalErrorAlert('Поймана глобальная ошибка UI', { + message: event.message || 'Global JS error', + stack: event.error?.stack || '', + sourceUrl: event.filename || '', + lineNumber: event.lineno, + columnNumber: event.colno, + pageId, + }); }); window.addEventListener('unhandledrejection', (event) => { const reason = event.reason; + const pageId = getRoute().pageId || ''; captureClientError({ kind: 'unhandled_rejection', message: reason?.message || String(reason || 'Unhandled promise rejection'), stack: reason?.stack || '', context: { - pageId: getRoute().pageId || '', + pageId, reasonType: reason?.constructor?.name || typeof reason, }, }); + + showGlobalErrorAlert('Пойман необработанный Promise reject', { + message: reason?.message || String(reason || 'Unhandled promise rejection'), + stack: reason?.stack || '', + pageId, + reasonType: reason?.constructor?.name || typeof reason, + }); }); function renderApp() { diff --git a/shine-UI/js/components/toolbar.js b/shine-UI/js/components/toolbar.js index 1c5484f..7378e6b 100644 --- a/shine-UI/js/components/toolbar.js +++ b/shine-UI/js/components/toolbar.js @@ -1,4 +1,4 @@ -import { resolveToolbarActive } from '../router.js?v=20260405171816'; +import { resolveToolbarActive } from '../router.js?v=20260407105357'; const ITEMS = [ { pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' }, diff --git a/shine-UI/js/mock-data.js b/shine-UI/js/mock-data.js index 87b11bd..3ce0540 100644 --- a/shine-UI/js/mock-data.js +++ b/shine-UI/js/mock-data.js @@ -1,6 +1,6 @@ export const profile = { login: '@shine.alex', - name: 'Алексей сияющий', + name: '', avatarInitials: 'АС', phone: '+7 (916) 221-45-88', address: 'Москва, Пресненская наб., 12', diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js index 4177aa5..4c35ea3 100644 --- a/shine-UI/js/pages/add-channel-view.js +++ b/shine-UI/js/pages/add-channel-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; export const pageMeta = { id: 'add-channel-view', title: 'Добавить канал' }; diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index ab8bc91..9a0bc0c 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { channelPosts, channels } from '../mock-data.js?v=20260405171816'; -import { addLocalChannelPost, authService, getLocalChannelPosts, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { channelPosts, channels } from '../mock-data.js?v=20260407105357'; +import { addLocalChannelPost, authService, getLocalChannelPosts, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'channel-view', title: 'Канал' }; diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index cc3d90f..c5d9802 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { directMessages } from '../mock-data.js?v=20260405171816'; -import { addChatMessage, getChatMessages, authService, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { directMessages } from '../mock-data.js?v=20260407105357'; +import { addChatMessage, getChatMessages, authService, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'chat-view', title: 'Чат' }; diff --git a/shine-UI/js/pages/connect-device-view.js b/shine-UI/js/pages/connect-device-view.js index ac768cc..e1c5def 100644 --- a/shine-UI/js/pages/connect-device-view.js +++ b/shine-UI/js/pages/connect-device-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'connect-device-view', title: 'Подключить устройство' }; diff --git a/shine-UI/js/pages/contact-search-view.js b/shine-UI/js/pages/contact-search-view.js index b5402c7..26c7fbf 100644 --- a/shine-UI/js/pages/contact-search-view.js +++ b/shine-UI/js/pages/contact-search-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { directMessages } from '../mock-data.js?v=20260405171816'; -import { authService, ensureChat, setContacts, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { directMessages } from '../mock-data.js?v=20260407105357'; +import { authService, ensureChat, setContacts, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' }; diff --git a/shine-UI/js/pages/device-camera-view.js b/shine-UI/js/pages/device-camera-view.js index e12a1cc..5aeaf93 100644 --- a/shine-UI/js/pages/device-camera-view.js +++ b/shine-UI/js/pages/device-camera-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; export const pageMeta = { id: 'device-camera-view', title: 'Подключить через камеру' }; diff --git a/shine-UI/js/pages/device-qr-view.js b/shine-UI/js/pages/device-qr-view.js index 99aca4f..7e0731c 100644 --- a/shine-UI/js/pages/device-qr-view.js +++ b/shine-UI/js/pages/device-qr-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { profile } from '../mock-data.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { profile } from '../mock-data.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'device-qr-view', title: 'Показать QR-код' }; diff --git a/shine-UI/js/pages/device-session-view.js b/shine-UI/js/pages/device-session-view.js index 39763fe..6b41d26 100644 --- a/shine-UI/js/pages/device-session-view.js +++ b/shine-UI/js/pages/device-session-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; import { authService, isSessionInvalidError, @@ -6,7 +6,7 @@ import { setAuthError, state, terminateCurrentSession, -} from '../state.js?v=20260405171816'; +} from '../state.js?v=20260407105357'; export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' }; diff --git a/shine-UI/js/pages/device-view.js b/shine-UI/js/pages/device-view.js index 7e47e18..e08841d 100644 --- a/shine-UI/js/pages/device-view.js +++ b/shine-UI/js/pages/device-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; import { authService, isSessionInvalidError, @@ -7,7 +7,7 @@ import { setAuthInfo, state, terminateCurrentSession, -} from '../state.js?v=20260405171816'; +} from '../state.js?v=20260407105357'; export const pageMeta = { id: 'device-view', title: 'Устройства' }; diff --git a/shine-UI/js/pages/entry-settings-view.js b/shine-UI/js/pages/entry-settings-view.js index 57f4234..64f69e7 100644 --- a/shine-UI/js/pages/entry-settings-view.js +++ b/shine-UI/js/pages/entry-settings-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false }; diff --git a/shine-UI/js/pages/key-storage-view.js b/shine-UI/js/pages/key-storage-view.js index 627b08a..64314c2 100644 --- a/shine-UI/js/pages/key-storage-view.js +++ b/shine-UI/js/pages/key-storage-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { authorizeSession, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { authorizeSession, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false }; diff --git a/shine-UI/js/pages/language-view.js b/shine-UI/js/pages/language-view.js index b4d7d3c..15dcd9d 100644 --- a/shine-UI/js/pages/language-view.js +++ b/shine-UI/js/pages/language-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'language-view', title: 'Язык' }; diff --git a/shine-UI/js/pages/login-camera-view.js b/shine-UI/js/pages/login-camera-view.js index 05f01d7..5489bbd 100644 --- a/shine-UI/js/pages/login-camera-view.js +++ b/shine-UI/js/pages/login-camera-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; export const pageMeta = { id: 'login-camera-view', title: 'Войти по камере', showAppChrome: false }; diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index 5dd99e1..9178d15 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -1,11 +1,11 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; import { authService, clearAuthMessages, setAuthBusy, setAuthError, state, -} from '../state.js?v=20260405171816'; +} from '../state.js?v=20260407105357'; export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false }; diff --git a/shine-UI/js/pages/login-view.js b/shine-UI/js/pages/login-view.js index 1cfb7ac..7838453 100644 --- a/shine-UI/js/pages/login-view.js +++ b/shine-UI/js/pages/login-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; export const pageMeta = { id: 'login-view', title: 'Войти', showAppChrome: false }; diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index 5ecf732..c961a27 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { directMessages } from '../mock-data.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { directMessages } from '../mock-data.js?v=20260407105357'; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index 082be41..95deefc 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { authService, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { authService, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'network-view', title: 'Связи' }; diff --git a/shine-UI/js/pages/notifications-view.js b/shine-UI/js/pages/notifications-view.js index 430df94..ae0c515 100644 --- a/shine-UI/js/pages/notifications-view.js +++ b/shine-UI/js/pages/notifications-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { notifications } from '../mock-data.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { notifications } from '../mock-data.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'notifications-view', title: 'Уведомления' }; diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js index d4ff8c0..0bec741 100644 --- a/shine-UI/js/pages/profile-view.js +++ b/shine-UI/js/pages/profile-view.js @@ -1,27 +1,27 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { profile } from '../mock-data.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { profile } from '../mock-data.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; import { loadProfileSnapshot, saveProfileParamBlock, saveProfileToggle, -} from '../services/user-profile-params.js?v=20260405171816'; +} from '../services/user-profile-params.js?v=20260407105357'; export const pageMeta = { id: 'profile-view', title: 'Профиль' }; -function getDisplayName(fields) { - const firstName = fields.find((field) => field.key === 'first_name')?.value?.trim() || ''; - const lastName = fields.find((field) => field.key === 'last_name')?.value?.trim() || ''; - const fullName = `${firstName} ${lastName}`.trim(); - return fullName || profile.name; -} - function toggleText(enabled) { return enabled ? 'Yes' : 'No'; } +function showLocalErrorAlert(prefix, error) { + const message = error?.message || 'Неизвестная ошибка'; + const stack = error?.stack ? `\n\nStack:\n${error.stack}` : ''; + window.alert(`${prefix}: ${message}${stack}`); +} + export function render({ navigate }) { const login = state.session.login || profile.login; + const displayLogin = String(login || '').toUpperCase(); const screen = document.createElement('section'); screen.className = 'stack'; @@ -45,8 +45,7 @@ export function render({ navigate }) {
${profile.avatarInitials}
-

${profile.name}

-

${login}

+

${displayLogin}

@@ -59,13 +58,6 @@ export function render({ navigate }) { `; - const hint = document.createElement('div'); - hint.className = 'card profile-data-help'; - hint.innerHTML = ` -
Личные данные пользователя
-

Параметры читаются через API GetUserParam. Изменения записываются в блокчейн и после этого список сразу обновляется.

- `; - const status = document.createElement('div'); status.className = 'status-line'; status.textContent = 'Загрузка параметров...'; @@ -73,7 +65,6 @@ export function render({ navigate }) { const listWrap = document.createElement('div'); listWrap.className = 'stack profile-param-list'; - const profileNameEl = topRow.querySelector('[data-profile-name="true"]'); const reloadBtn = topRow.querySelector('[data-reload="true"]'); const officialBtn = badgesRow.querySelector('[data-toggle="official"]'); const shineBtn = badgesRow.querySelector('[data-toggle="shine"]'); @@ -105,15 +96,15 @@ export function render({ navigate }) { } function renderFields(fields) { - profileNameEl.textContent = getDisplayName(fields); - listWrap.innerHTML = ''; fields.forEach((field) => { const row = document.createElement('div'); row.className = 'card profile-param-item row'; const value = String(field.value || '').trim() || 'не заполнено'; + const isNameField = field.key === 'first_name' || field.key === 'last_name'; + const valueClass = isNameField ? 'profile-param-value profile-param-value-small' : 'profile-param-value'; row.innerHTML = ` -
${field.label}: ${value}
+
${field.label}: ${value}
`; listWrap.append(row); @@ -140,6 +131,7 @@ export function render({ navigate }) { } catch (error) { status.className = 'status-line is-unavailable'; status.textContent = `Не удалось загрузить параметры: ${error.message || 'ошибка сети'}`; + showLocalErrorAlert('Ошибка загрузки параметров профиля', error); } finally { reloadBtn.disabled = false; officialBtn.disabled = false; @@ -167,6 +159,7 @@ export function render({ navigate }) { } catch (error) { status.className = 'status-line is-unavailable'; status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`; + showLocalErrorAlert(`Ошибка изменения ${toggleKey}`, error); } } @@ -191,6 +184,7 @@ export function render({ navigate }) { } catch (error) { status.className = 'status-line is-unavailable'; status.textContent = `Не удалось изменить ${field.key}: ${error.message || 'ошибка сети'}`; + showLocalErrorAlert(`Ошибка изменения ${field.key}`, error); } } @@ -206,7 +200,7 @@ export function render({ navigate }) { officialBtn.addEventListener('click', () => onToggleClick('official')); shineBtn.addEventListener('click', () => onToggleClick('shine')); - card.append(topRow, badgesRow, hint, status, listWrap); + card.append(topRow, badgesRow, status, listWrap); screen.append(card); refreshProfileSnapshot(); diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index dcf6e12..4000b6c 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { authService, clearAuthMessages, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { authService, clearAuthMessages, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; diff --git a/shine-UI/js/pages/registration-keys-view.js b/shine-UI/js/pages/registration-keys-view.js index 8f7a69c..0c430fd 100644 --- a/shine-UI/js/pages/registration-keys-view.js +++ b/shine-UI/js/pages/registration-keys-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; import { authService, authorizeSession, @@ -6,7 +6,7 @@ import { setAuthError, setAuthInfo, state, -} from '../state.js?v=20260405171816'; +} from '../state.js?v=20260407105357'; export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false }; diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index 2f4a4f9..104ab55 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -1,11 +1,11 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; import { authService, refreshRegistrationBalance, setAuthError, setAuthInfo, state, -} from '../state.js?v=20260405171816'; +} from '../state.js?v=20260407105357'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; diff --git a/shine-UI/js/pages/server-settings-view.js b/shine-UI/js/pages/server-settings-view.js index a4d107a..a80db3d 100644 --- a/shine-UI/js/pages/server-settings-view.js +++ b/shine-UI/js/pages/server-settings-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' }; diff --git a/shine-UI/js/pages/settings-view.js b/shine-UI/js/pages/settings-view.js index 1813dd8..f879b9a 100644 --- a/shine-UI/js/pages/settings-view.js +++ b/shine-UI/js/pages/settings-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; export const pageMeta = { id: 'settings-view', title: 'Настройки' }; diff --git a/shine-UI/js/pages/show-keys-view.js b/shine-UI/js/pages/show-keys-view.js index 433c37f..1e44908 100644 --- a/shine-UI/js/pages/show-keys-view.js +++ b/shine-UI/js/pages/show-keys-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; -import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; +import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260407105357'; export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' }; diff --git a/shine-UI/js/pages/start-view.js b/shine-UI/js/pages/start-view.js index 32fb4e1..3596063 100644 --- a/shine-UI/js/pages/start-view.js +++ b/shine-UI/js/pages/start-view.js @@ -1,4 +1,4 @@ -import { clearStartHint, state } from '../state.js?v=20260405171816'; +import { clearStartHint, state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'start-view', title: 'Старт', showAppChrome: false }; diff --git a/shine-UI/js/pages/topup-view.js b/shine-UI/js/pages/topup-view.js index 8414ceb..8005561 100644 --- a/shine-UI/js/pages/topup-view.js +++ b/shine-UI/js/pages/topup-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { state } from '../state.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { state } from '../state.js?v=20260407105357'; export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false }; diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index 9233863..7ed7fbf 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260405171816'; -import { wallet } from '../mock-data.js?v=20260405171816'; +import { renderHeader } from '../components/header.js?v=20260407105357'; +import { wallet } from '../mock-data.js?v=20260407105357'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 1b6ea4f..0abde9e 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -1,4 +1,4 @@ -import { WsJsonClient } from './ws-client.js?v=20260405171816'; +import { WsJsonClient } from './ws-client.js?v=20260407105357'; import { bytesToBase64, deriveEd25519FromPassword, @@ -11,13 +11,13 @@ import { signBytes, signBase64, utf8Bytes, -} from './crypto-utils.js?v=20260405171816'; +} from './crypto-utils.js?v=20260407105357'; import { loadEncryptedUserSecrets, loadSessionMaterial, saveEncryptedUserSecrets, saveSessionMaterial, -} from './key-vault.js?v=20260405171816'; +} from './key-vault.js?v=20260407105357'; const BCH_SUFFIX = '001'; @@ -378,6 +378,12 @@ export class AuthService { const user = await this.getUser(cleanLogin); const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim(); + const freshNum = Number(user?.serverLastGlobalNumber); + const freshHash = String(user?.serverLastGlobalHash || '').trim().toLowerCase(); + const freshCursor = { + serverLastGlobalNumber: Number.isFinite(freshNum) ? freshNum : -1, + serverLastGlobalHash: freshHash.length === 64 ? freshHash : '0'.repeat(64), + }; const savedKeys = await loadEncryptedUserSecrets(cleanLogin, storagePwd); const blockchainPrivatePkcs8 = savedKeys?.blockchainKey; @@ -391,11 +397,14 @@ export class AuthService { const blockNumber = Number(cursor?.serverLastGlobalNumber ?? -1) + 1; const prevBlockHash = String(cursor?.serverLastGlobalHash || '0'.repeat(64)); + // Для USER_PARAM отправляем старт новой line-цепочки: + // prevLineNumber=-1, prevLineHash=0x00..00, thisLineNumber=-1. + // Этот формат соответствует BodyHasLine правилам на сервере. const bodyBytes = makeUserParamBodyBytes({ - lineCode: Number(cursor?.serverLastGlobalNumber ?? 0), - prevLineNumber: Number(cursor?.serverLastGlobalNumber ?? 0), - prevLineHashHex: prevBlockHash, - thisLineNumber: 1, + lineCode: 0, + prevLineNumber: -1, + prevLineHashHex: '0'.repeat(64), + thisLineNumber: -1, key: cleanParam, value: cleanValue, }); @@ -426,7 +435,7 @@ export class AuthService { return response; }; - let cursor = { serverLastGlobalNumber: -1, serverLastGlobalHash: '0'.repeat(64) }; + let cursor = freshCursor; let response = await tryAdd(cursor); if (response.status !== 200) { const knownNum = Number(response?.payload?.serverLastGlobalNumber); diff --git a/shine-UI/js/services/key-vault.js b/shine-UI/js/services/key-vault.js index e41b8fe..ed13975 100644 --- a/shine-UI/js/services/key-vault.js +++ b/shine-UI/js/services/key-vault.js @@ -1,7 +1,7 @@ import { decryptJsonWithStoragePwd, encryptJsonWithStoragePwd, -} from './crypto-utils.js?v=20260405171816'; +} from './crypto-utils.js?v=20260407105357'; const DB_NAME = 'shine-ui-auth'; const DB_VERSION = 1; diff --git a/shine-UI/js/services/user-profile-params.js b/shine-UI/js/services/user-profile-params.js index 2d0ca35..f614004 100644 --- a/shine-UI/js/services/user-profile-params.js +++ b/shine-UI/js/services/user-profile-params.js @@ -1,4 +1,4 @@ -import { authService, state } from '../state.js?v=20260403081123'; +import { authService, state } from '../state.js?v=20260407105357'; export const profileFieldDefs = [ { key: 'first_name', readKeys: ['first_name'], label: 'Имя', placeholder: 'Введите имя' }, diff --git a/shine-UI/js/services/ws-client.js b/shine-UI/js/services/ws-client.js index 90ad022..6576c4d 100644 --- a/shine-UI/js/services/ws-client.js +++ b/shine-UI/js/services/ws-client.js @@ -1,4 +1,4 @@ -import { captureClientError } from './client-error-reporter.js?v=20260405171816'; +import { captureClientError } from './client-error-reporter.js?v=20260407105357'; const DEFAULT_TIMEOUT_MS = 12000; diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js index fb20698..f47d048 100644 --- a/shine-UI/js/state.js +++ b/shine-UI/js/state.js @@ -1,6 +1,6 @@ -import { chatMessages, wallet } from './mock-data.js?v=20260405171816'; -import { AuthService } from './services/auth-service.js?v=20260405171816'; -import { clearClientAuthData } from './services/key-vault.js?v=20260405171816'; +import { chatMessages, wallet } from './mock-data.js?v=20260407105357'; +import { AuthService } from './services/auth-service.js?v=20260407105357'; +import { clearClientAuthData } from './services/key-vault.js?v=20260407105357'; const clone = (value) => JSON.parse(JSON.stringify(value)); const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1'; diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 6459e5e..92cb1b3 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -177,6 +177,10 @@ word-break: break-word; } +.profile-param-value-small { + font-size: 13px; +} + .profile-param-time { font-size: 12px; } @@ -408,6 +412,10 @@ .avatar { width: 44px; height: 44px; + min-width: 44px; + min-height: 44px; + flex: 0 0 auto; + aspect-ratio: 1 / 1; border-radius: 50%; background: linear-gradient(130deg, #3c4f73, #243352); display: grid; diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index 92206d1..09bc0f3 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -6,6 +6,7 @@ import blockchain.MsgSubType; import blockchain.body.BodyHasLine; import blockchain.body.BodyHasTarget; import blockchain.body.CreateChannelBody; +import blockchain.body.UserParamBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import server.logic.ws_protocol.Base64Ws; @@ -21,8 +22,10 @@ import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_R import server.logic.ws_protocol.WireCodes; import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlocksDAO; +import shine.db.dao.UserParamsDAO; import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockEntry; +import shine.db.entities.UserParamEntry; import utils.blockchain.BlockchainNameUtil; import java.util.Arrays; @@ -45,8 +48,9 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); + private final UserParamsDAO userParamsDAO = UserParamsDAO.getInstance(); - private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO); + private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO, userParamsDAO); @Override public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) { @@ -370,7 +374,22 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { be.setEditedByBlockNumber(be.getToBlockNumber()); } - dbWriter.appendBlockAndState(blockchainName, block, st, be); + UserParamEntry upsertedParam = null; + if (block.body instanceof UserParamBody upBody) { + String effectiveLogin = (st.getLogin() != null && !st.getLogin().isBlank()) + ? st.getLogin() + : login; + upsertedParam = new UserParamEntry( + effectiveLogin, + upBody.paramKey, + block.timestamp * 1000L, + upBody.paramValue, + null, + null + ); + } + + dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam); } catch (Exception e) { log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})", diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java index dc344c6..40ce676 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java @@ -3,8 +3,10 @@ package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_u import blockchain.BchBlockEntry; import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlocksDAO; +import shine.db.dao.UserParamsDAO; import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockEntry; +import shine.db.entities.UserParamEntry; import utils.files.FileStoreUtil; import java.sql.Connection; @@ -21,17 +23,20 @@ public final class BlockchainWriter { private final BlocksDAO blocksDAO; private final BlockchainStateDAO stateDAO; + private final UserParamsDAO userParamsDAO; private final FileStoreUtil fs = FileStoreUtil.getInstance(); - public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) { + public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO, UserParamsDAO userParamsDAO) { this.blocksDAO = blocksDAO; this.stateDAO = stateDAO; + this.userParamsDAO = userParamsDAO; } public void appendBlockAndState(String blockchainName, BchBlockEntry block, BlockchainStateEntry st, - BlockEntry be) throws SQLException { + BlockEntry be, + UserParamEntry userParamEntry) throws SQLException { long nowMs = System.currentTimeMillis(); @@ -49,6 +54,11 @@ public final class BlockchainWriter { stateDAO.upsert(c, st); + // 2.1) Если блок — USER_PARAM, синхронизируем снимок в users_params в той же транзакции. + if (userParamEntry != null) { + userParamsDAO.upsertIfNewer(c, userParamEntry); + } + c.commit(); } catch (Exception e) { try { c.rollback(); } catch (Exception ignored) {} @@ -64,4 +74,4 @@ public final class BlockchainWriter { String fileName = fs.buildBlockchainFileName(blockchainName); fs.addDataToFile(fileName, block.toBytes()); } -} \ No newline at end of file +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java index 58f49b7..3b17035 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java @@ -10,10 +10,13 @@ import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Re import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; +import shine.db.dao.BlockchainStateDAO; import shine.db.dao.SolanaUsersDAO; +import shine.db.entities.BlockchainStateEntry; import shine.db.entities.SolanaUserEntry; import java.sql.SQLException; +import java.util.Arrays; public class Net_GetUser_Handler implements JsonMessageHandler { @@ -35,6 +38,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler { } SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); + BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); try { SolanaUserEntry u = usersDAO.getByLogin(req.getLogin()); @@ -60,6 +64,32 @@ public class Net_GetUser_Handler implements JsonMessageHandler { resp.setBlockchainKey(u.getBlockchainKey()); resp.setDeviceKey(u.getDeviceKey()); + // Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна, + // автоматически восстанавливаем её для существующего пользователя. + BlockchainStateEntry st = stateDAO.getByBlockchainName(u.getBlockchainName()); + if (st == null) { + st = new BlockchainStateEntry(); + st.setBlockchainName(u.getBlockchainName()); + st.setLogin(u.getLogin()); + st.setBlockchainKey(u.getBlockchainKey()); + st.setLastBlockNumber(-1); + st.setLastBlockHash(new byte[32]); + st.setFileSizeBytes(0); + st.setSizeLimit(1_000_000L); + st.setUpdatedAtMs(System.currentTimeMillis()); + stateDAO.upsert(st); + log.warn("GetUser: восстановлена запись blockchain_state для login={}, blockchainName={}", + u.getLogin(), u.getBlockchainName()); + } + + int lastNum = st.getLastBlockNumber(); + byte[] lastHash = st.getLastBlockHash(); + if (lastHash == null || lastHash.length != 32) { + lastHash = new byte[32]; + } + resp.setServerLastGlobalNumber(lastNum); + resp.setServerLastGlobalHash(toHex32(lastHash)); + log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName()); return resp; @@ -81,4 +111,16 @@ public class Net_GetUser_Handler implements JsonMessageHandler { ); } } + + private static String toHex32(byte[] bytes32) { + byte[] b = (bytes32 == null) ? new byte[32] : Arrays.copyOf(bytes32, 32); + final char[] HEX = "0123456789abcdef".toCharArray(); + char[] out = new char[64]; + for (int i = 0; i < 32; i++) { + int v = b[i] & 0xFF; + out[i * 2] = HEX[v >>> 4]; + out[i * 2 + 1] = HEX[v & 0x0F]; + } + return new String(out); + } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java index 73c55cd..88ce05f 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java @@ -39,6 +39,8 @@ public class Net_GetUser_Response extends Net_Response { private String solanaKey; private String blockchainKey; private String deviceKey; + private Integer serverLastGlobalNumber; + private String serverLastGlobalHash; public Boolean getExists() { return exists; } public void setExists(Boolean exists) { this.exists = exists; } @@ -57,4 +59,10 @@ public class Net_GetUser_Response extends Net_Response { public String getDeviceKey() { return deviceKey; } public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } -} \ No newline at end of file + + public Integer getServerLastGlobalNumber() { return serverLastGlobalNumber; } + public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) { this.serverLastGlobalNumber = serverLastGlobalNumber; } + + public String getServerLastGlobalHash() { return serverLastGlobalHash; } + public void setServerLastGlobalHash(String serverLastGlobalHash) { this.serverLastGlobalHash = serverLastGlobalHash; } +}