Закомитил промежуточную почти работающую версию ...

This commit is contained in:
AidarKC 2026-04-07 13:57:09 +03:00
parent 4deaedf79f
commit 3016d25f73
43 changed files with 271 additions and 143 deletions

View File

@ -182,10 +182,18 @@ tasks.register('deployServer', JavaExec) {
dependsOn testClasses dependsOn testClasses
} }
tasks.register('deployPWA', Exec) { tasks.register('deployWEB', Exec) {
group = "!!deployment" group = "!!deployment"
description = "Deploy PWA via deploy_shine-PWA.sh" description = "Deploy WEB via deploy_shine-PWA.sh"
workingDir = rootDir workingDir = rootDir
commandLine 'bash', file('deploy_shine-PWA.sh').absolutePath 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')
}

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="manifest" href="./manifest.webmanifest" /> <link rel="manifest" href="./manifest.webmanifest" />
<title>Shine UI Demo</title> <title>Shine UI Demo</title>
<link rel="stylesheet" href="./styles/main.css?v=20260405171816" /> <link rel="stylesheet" href="./styles/main.css?v=20260407105357" />
<link rel="stylesheet" href="./styles/layout.css?v=20260405171816" /> <link rel="stylesheet" href="./styles/layout.css?v=20260407105357" />
<link rel="stylesheet" href="./styles/components.css?v=20260405171816" /> <link rel="stylesheet" href="./styles/components.css?v=20260407105357" />
</head> </head>
<body> <body>
<div class="app-shell"> <div class="app-shell">
@ -27,6 +27,6 @@
}; };
window.__SHINE_FIREBASE_VAPID_KEY__ = ''; window.__SHINE_FIREBASE_VAPID_KEY__ = '';
</script> </script>
<script type="module" src="./js/app.js?v=20260405171816"></script> <script type="module" src="./js/app.js?v=20260407105357"></script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,7 @@
import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260405171816'; import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260407105357';
import { renderToolbar } from './components/toolbar.js?v=20260405171816'; import { renderToolbar } from './components/toolbar.js?v=20260407105357';
import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js?v=20260405171816'; import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js?v=20260407105357';
import { initPwaPush } from './services/pwa-push-service.js?v=20260405171816'; import { initPwaPush } from './services/pwa-push-service.js?v=20260407105357';
import { import {
authService, authService,
authorizeSession, authorizeSession,
@ -12,38 +12,38 @@ import {
terminateCurrentSession, terminateCurrentSession,
addIncomingMessage, addIncomingMessage,
setContacts, setContacts,
} from './state.js?v=20260405171816'; } from './state.js?v=20260407105357';
import * as startView from './pages/start-view.js?v=20260405171816'; import * as startView from './pages/start-view.js?v=20260407105357';
import * as entrySettingsView from './pages/entry-settings-view.js?v=20260405171816'; import * as entrySettingsView from './pages/entry-settings-view.js?v=20260407105357';
import * as registerView from './pages/register-view.js?v=20260405171816'; import * as registerView from './pages/register-view.js?v=20260407105357';
import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260405171816'; import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260407105357';
import * as registrationKeysView from './pages/registration-keys-view.js?v=20260405171816'; import * as registrationKeysView from './pages/registration-keys-view.js?v=20260407105357';
import * as topupView from './pages/topup-view.js?v=20260405171816'; import * as topupView from './pages/topup-view.js?v=20260407105357';
import * as loginView from './pages/login-view.js?v=20260405171816'; import * as loginView from './pages/login-view.js?v=20260407105357';
import * as loginCameraView from './pages/login-camera-view.js?v=20260405171816'; import * as loginCameraView from './pages/login-camera-view.js?v=20260407105357';
import * as loginPasswordView from './pages/login-password-view.js?v=20260405171816'; import * as loginPasswordView from './pages/login-password-view.js?v=20260407105357';
import * as keyStorageView from './pages/key-storage-view.js?v=20260405171816'; import * as keyStorageView from './pages/key-storage-view.js?v=20260407105357';
import * as profileView from './pages/profile-view.js?v=20260405171816'; import * as profileView from './pages/profile-view.js?v=20260407105357';
import * as walletView from './pages/wallet-view.js?v=20260405171816'; import * as walletView from './pages/wallet-view.js?v=20260407105357';
import * as settingsView from './pages/settings-view.js?v=20260405171816'; import * as settingsView from './pages/settings-view.js?v=20260407105357';
import * as serverSettingsView from './pages/server-settings-view.js?v=20260405171816'; import * as serverSettingsView from './pages/server-settings-view.js?v=20260407105357';
import * as deviceView from './pages/device-view.js?v=20260405171816'; import * as deviceView from './pages/device-view.js?v=20260407105357';
import * as connectDeviceView from './pages/connect-device-view.js?v=20260405171816'; import * as connectDeviceView from './pages/connect-device-view.js?v=20260407105357';
import * as deviceQrView from './pages/device-qr-view.js?v=20260405171816'; import * as deviceQrView from './pages/device-qr-view.js?v=20260407105357';
import * as deviceCameraView from './pages/device-camera-view.js?v=20260405171816'; import * as deviceCameraView from './pages/device-camera-view.js?v=20260407105357';
import * as showKeysView from './pages/show-keys-view.js?v=20260405171816'; import * as showKeysView from './pages/show-keys-view.js?v=20260407105357';
import * as deviceSessionView from './pages/device-session-view.js?v=20260405171816'; import * as deviceSessionView from './pages/device-session-view.js?v=20260407105357';
import * as languageView from './pages/language-view.js?v=20260405171816'; import * as languageView from './pages/language-view.js?v=20260407105357';
import * as messagesList from './pages/messages-list.js?v=20260405171816'; import * as messagesList from './pages/messages-list.js?v=20260407105357';
import * as contactSearchView from './pages/contact-search-view.js?v=20260405171816'; import * as contactSearchView from './pages/contact-search-view.js?v=20260407105357';
import * as chatView from './pages/chat-view.js?v=20260405171816'; import * as chatView from './pages/chat-view.js?v=20260407105357';
import * as channelsList from './pages/channels-list.js?v=20260405171816'; import * as channelsList from './pages/channels-list.js?v=20260407105357';
import * as channelView from './pages/channel-view.js?v=20260405171816'; import * as channelView from './pages/channel-view.js?v=20260407105357';
import * as addChannelView from './pages/add-channel-view.js?v=20260405171816'; import * as addChannelView from './pages/add-channel-view.js?v=20260407105357';
import * as networkView from './pages/network-view.js?v=20260405171816'; import * as networkView from './pages/network-view.js?v=20260407105357';
import * as notificationsView from './pages/notifications-view.js?v=20260405171816'; import * as notificationsView from './pages/notifications-view.js?v=20260407105357';
const routes = { const routes = {
'start-view': startView, 'start-view': startView,
@ -84,7 +84,20 @@ let currentCleanup = null;
setClientErrorTransport((payload) => authService.reportClientError(payload)); 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) => { window.addEventListener('error', (event) => {
const pageId = getRoute().pageId || '';
captureClientError({ captureClientError({
kind: 'global_error', kind: 'global_error',
message: event.message || 'Global JS error', message: event.message || 'Global JS error',
@ -93,22 +106,39 @@ window.addEventListener('error', (event) => {
lineNumber: event.lineno, lineNumber: event.lineno,
columnNumber: event.colno, columnNumber: event.colno,
context: { 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) => { window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason; const reason = event.reason;
const pageId = getRoute().pageId || '';
captureClientError({ captureClientError({
kind: 'unhandled_rejection', kind: 'unhandled_rejection',
message: reason?.message || String(reason || 'Unhandled promise rejection'), message: reason?.message || String(reason || 'Unhandled promise rejection'),
stack: reason?.stack || '', stack: reason?.stack || '',
context: { context: {
pageId: getRoute().pageId || '', pageId,
reasonType: reason?.constructor?.name || typeof reason, 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() { function renderApp() {

View File

@ -1,4 +1,4 @@
import { resolveToolbarActive } from '../router.js?v=20260405171816'; import { resolveToolbarActive } from '../router.js?v=20260407105357';
const ITEMS = [ const ITEMS = [
{ pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' }, { pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' },

View File

@ -1,6 +1,6 @@
export const profile = { export const profile = {
login: '@shine.alex', login: '@shine.alex',
name: 'Алексей сияющий', name: '',
avatarInitials: 'АС', avatarInitials: 'АС',
phone: '+7 (916) 221-45-88', phone: '+7 (916) 221-45-88',
address: 'Москва, Пресненская наб., 12', address: 'Москва, Пресненская наб., 12',

View File

@ -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: 'Добавить канал' }; export const pageMeta = { id: 'add-channel-view', title: 'Добавить канал' };

View File

@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { channelPosts, channels } from '../mock-data.js?v=20260405171816'; import { channelPosts, channels } from '../mock-data.js?v=20260407105357';
import { addLocalChannelPost, authService, getLocalChannelPosts, state } from '../state.js?v=20260405171816'; import { addLocalChannelPost, authService, getLocalChannelPosts, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'channel-view', title: 'Канал' }; export const pageMeta = { id: 'channel-view', title: 'Канал' };

View File

@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { directMessages } from '../mock-data.js?v=20260405171816'; import { directMessages } from '../mock-data.js?v=20260407105357';
import { addChatMessage, getChatMessages, authService, state } from '../state.js?v=20260405171816'; import { addChatMessage, getChatMessages, authService, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'chat-view', title: 'Чат' }; export const pageMeta = { id: 'chat-view', title: 'Чат' };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'connect-device-view', title: 'Подключить устройство' }; export const pageMeta = { id: 'connect-device-view', title: 'Подключить устройство' };

View File

@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { directMessages } from '../mock-data.js?v=20260405171816'; import { directMessages } from '../mock-data.js?v=20260407105357';
import { authService, ensureChat, setContacts, state } from '../state.js?v=20260405171816'; import { authService, ensureChat, setContacts, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' }; export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' };

View File

@ -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: 'Подключить через камеру' }; export const pageMeta = { id: 'device-camera-view', title: 'Подключить через камеру' };

View File

@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { profile } from '../mock-data.js?v=20260405171816'; import { profile } from '../mock-data.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'device-qr-view', title: 'Показать QR-код' }; export const pageMeta = { id: 'device-qr-view', title: 'Показать QR-код' };

View File

@ -1,4 +1,4 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { import {
authService, authService,
isSessionInvalidError, isSessionInvalidError,
@ -6,7 +6,7 @@ import {
setAuthError, setAuthError,
state, state,
terminateCurrentSession, terminateCurrentSession,
} from '../state.js?v=20260405171816'; } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' }; export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' };

View File

@ -1,4 +1,4 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { import {
authService, authService,
isSessionInvalidError, isSessionInvalidError,
@ -7,7 +7,7 @@ import {
setAuthInfo, setAuthInfo,
state, state,
terminateCurrentSession, terminateCurrentSession,
} from '../state.js?v=20260405171816'; } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'device-view', title: 'Устройства' }; export const pageMeta = { id: 'device-view', title: 'Устройства' };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260405171816'; import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false }; export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { authorizeSession, state } from '../state.js?v=20260405171816'; import { authorizeSession, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false }; export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'language-view', title: 'Язык' }; export const pageMeta = { id: 'language-view', title: 'Язык' };

View File

@ -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 }; export const pageMeta = { id: 'login-camera-view', title: 'Войти по камере', showAppChrome: false };

View File

@ -1,11 +1,11 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { import {
authService, authService,
clearAuthMessages, clearAuthMessages,
setAuthBusy, setAuthBusy,
setAuthError, setAuthError,
state, state,
} from '../state.js?v=20260405171816'; } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false }; export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false };

View File

@ -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 }; export const pageMeta = { id: 'login-view', title: 'Войти', showAppChrome: false };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { directMessages } from '../mock-data.js?v=20260405171816'; import { directMessages } from '../mock-data.js?v=20260407105357';
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { authService, state } from '../state.js?v=20260405171816'; import { authService, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'network-view', title: 'Связи' }; export const pageMeta = { id: 'network-view', title: 'Связи' };

View File

@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { notifications } from '../mock-data.js?v=20260405171816'; import { notifications } from '../mock-data.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'notifications-view', title: 'Уведомления' }; export const pageMeta = { id: 'notifications-view', title: 'Уведомления' };

View File

@ -1,27 +1,27 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { profile } from '../mock-data.js?v=20260405171816'; import { profile } from '../mock-data.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
import { import {
loadProfileSnapshot, loadProfileSnapshot,
saveProfileParamBlock, saveProfileParamBlock,
saveProfileToggle, saveProfileToggle,
} from '../services/user-profile-params.js?v=20260405171816'; } from '../services/user-profile-params.js?v=20260407105357';
export const pageMeta = { id: 'profile-view', title: 'Профиль' }; 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) { function toggleText(enabled) {
return enabled ? 'Yes' : 'No'; 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 }) { export function render({ navigate }) {
const login = state.session.login || profile.login; const login = state.session.login || profile.login;
const displayLogin = String(login || '').toUpperCase();
const screen = document.createElement('section'); const screen = document.createElement('section');
screen.className = 'stack'; screen.className = 'stack';
@ -45,8 +45,7 @@ export function render({ navigate }) {
<div class="row" style="gap:12px; align-items:center;"> <div class="row" style="gap:12px; align-items:center;">
<div class="avatar large">${profile.avatarInitials}</div> <div class="avatar large">${profile.avatarInitials}</div>
<div> <div>
<h2 style="font-size:22px; margin-bottom:2px;" data-profile-name="true">${profile.name}</h2> <h2 style="font-size:22px; margin-bottom:2px;" data-profile-login="true">${displayLogin}</h2>
<p class="meta-muted">${login}</p>
</div> </div>
</div> </div>
<button class="primary-btn" type="button" data-reload="true">Обновить</button> <button class="primary-btn" type="button" data-reload="true">Обновить</button>
@ -59,13 +58,6 @@ export function render({ navigate }) {
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="shine">Сияющий: No</button> <button class="badge profile-toggle-btn is-no" type="button" data-toggle="shine">Сияющий: No</button>
`; `;
const hint = document.createElement('div');
hint.className = 'card profile-data-help';
hint.innerHTML = `
<div class="meta-muted">Личные данные пользователя</div>
<p>Параметры читаются через API GetUserParam. Изменения записываются в блокчейн и после этого список сразу обновляется.</p>
`;
const status = document.createElement('div'); const status = document.createElement('div');
status.className = 'status-line'; status.className = 'status-line';
status.textContent = 'Загрузка параметров...'; status.textContent = 'Загрузка параметров...';
@ -73,7 +65,6 @@ export function render({ navigate }) {
const listWrap = document.createElement('div'); const listWrap = document.createElement('div');
listWrap.className = 'stack profile-param-list'; listWrap.className = 'stack profile-param-list';
const profileNameEl = topRow.querySelector('[data-profile-name="true"]');
const reloadBtn = topRow.querySelector('[data-reload="true"]'); const reloadBtn = topRow.querySelector('[data-reload="true"]');
const officialBtn = badgesRow.querySelector('[data-toggle="official"]'); const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]'); const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
@ -105,15 +96,15 @@ export function render({ navigate }) {
} }
function renderFields(fields) { function renderFields(fields) {
profileNameEl.textContent = getDisplayName(fields);
listWrap.innerHTML = ''; listWrap.innerHTML = '';
fields.forEach((field) => { fields.forEach((field) => {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'card profile-param-item row'; row.className = 'card profile-param-item row';
const value = String(field.value || '').trim() || 'не заполнено'; 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 = ` row.innerHTML = `
<div class="profile-param-value"><b>${field.label}</b>: ${value}</div> <div class="${valueClass}"><b>${field.label}</b>: ${value}</div>
<button class="ghost-btn" type="button" data-edit-field="${field.key}">Изменить</button> <button class="ghost-btn" type="button" data-edit-field="${field.key}">Изменить</button>
`; `;
listWrap.append(row); listWrap.append(row);
@ -140,6 +131,7 @@ export function render({ navigate }) {
} catch (error) { } catch (error) {
status.className = 'status-line is-unavailable'; status.className = 'status-line is-unavailable';
status.textContent = `Не удалось загрузить параметры: ${error.message || 'ошибка сети'}`; status.textContent = `Не удалось загрузить параметры: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert('Ошибка загрузки параметров профиля', error);
} finally { } finally {
reloadBtn.disabled = false; reloadBtn.disabled = false;
officialBtn.disabled = false; officialBtn.disabled = false;
@ -167,6 +159,7 @@ export function render({ navigate }) {
} catch (error) { } catch (error) {
status.className = 'status-line is-unavailable'; status.className = 'status-line is-unavailable';
status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`; status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert(`Ошибка изменения ${toggleKey}`, error);
} }
} }
@ -191,6 +184,7 @@ export function render({ navigate }) {
} catch (error) { } catch (error) {
status.className = 'status-line is-unavailable'; status.className = 'status-line is-unavailable';
status.textContent = `Не удалось изменить ${field.key}: ${error.message || 'ошибка сети'}`; status.textContent = `Не удалось изменить ${field.key}: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert(`Ошибка изменения ${field.key}`, error);
} }
} }
@ -206,7 +200,7 @@ export function render({ navigate }) {
officialBtn.addEventListener('click', () => onToggleClick('official')); officialBtn.addEventListener('click', () => onToggleClick('official'));
shineBtn.addEventListener('click', () => onToggleClick('shine')); shineBtn.addEventListener('click', () => onToggleClick('shine'));
card.append(topRow, badgesRow, hint, status, listWrap); card.append(topRow, badgesRow, status, listWrap);
screen.append(card); screen.append(card);
refreshProfileSnapshot(); refreshProfileSnapshot();

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { authService, clearAuthMessages, state } from '../state.js?v=20260405171816'; import { authService, clearAuthMessages, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };

View File

@ -1,4 +1,4 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { import {
authService, authService,
authorizeSession, authorizeSession,
@ -6,7 +6,7 @@ import {
setAuthError, setAuthError,
setAuthInfo, setAuthInfo,
state, state,
} from '../state.js?v=20260405171816'; } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false }; export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false };

View File

@ -1,11 +1,11 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { import {
authService, authService,
refreshRegistrationBalance, refreshRegistrationBalance,
setAuthError, setAuthError,
setAuthInfo, setAuthInfo,
state, state,
} from '../state.js?v=20260405171816'; } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260405171816'; import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' }; export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' };

View File

@ -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: 'Настройки' }; export const pageMeta = { id: 'settings-view', title: 'Настройки' };

View File

@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260405171816'; import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260407105357';
export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' }; export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' };

View File

@ -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 }; export const pageMeta = { id: 'start-view', title: 'Старт', showAppChrome: false };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { state } from '../state.js?v=20260405171816'; import { state } from '../state.js?v=20260407105357';
export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false }; export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false };

View File

@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js?v=20260405171816'; import { renderHeader } from '../components/header.js?v=20260407105357';
import { wallet } from '../mock-data.js?v=20260405171816'; import { wallet } from '../mock-data.js?v=20260407105357';
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };

View File

@ -1,4 +1,4 @@
import { WsJsonClient } from './ws-client.js?v=20260405171816'; import { WsJsonClient } from './ws-client.js?v=20260407105357';
import { import {
bytesToBase64, bytesToBase64,
deriveEd25519FromPassword, deriveEd25519FromPassword,
@ -11,13 +11,13 @@ import {
signBytes, signBytes,
signBase64, signBase64,
utf8Bytes, utf8Bytes,
} from './crypto-utils.js?v=20260405171816'; } from './crypto-utils.js?v=20260407105357';
import { import {
loadEncryptedUserSecrets, loadEncryptedUserSecrets,
loadSessionMaterial, loadSessionMaterial,
saveEncryptedUserSecrets, saveEncryptedUserSecrets,
saveSessionMaterial, saveSessionMaterial,
} from './key-vault.js?v=20260405171816'; } from './key-vault.js?v=20260407105357';
const BCH_SUFFIX = '001'; const BCH_SUFFIX = '001';
@ -378,6 +378,12 @@ export class AuthService {
const user = await this.getUser(cleanLogin); const user = await this.getUser(cleanLogin);
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim(); 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 savedKeys = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
const blockchainPrivatePkcs8 = savedKeys?.blockchainKey; const blockchainPrivatePkcs8 = savedKeys?.blockchainKey;
@ -391,11 +397,14 @@ export class AuthService {
const blockNumber = Number(cursor?.serverLastGlobalNumber ?? -1) + 1; const blockNumber = Number(cursor?.serverLastGlobalNumber ?? -1) + 1;
const prevBlockHash = String(cursor?.serverLastGlobalHash || '0'.repeat(64)); const prevBlockHash = String(cursor?.serverLastGlobalHash || '0'.repeat(64));
// Для USER_PARAM отправляем старт новой line-цепочки:
// prevLineNumber=-1, prevLineHash=0x00..00, thisLineNumber=-1.
// Этот формат соответствует BodyHasLine правилам на сервере.
const bodyBytes = makeUserParamBodyBytes({ const bodyBytes = makeUserParamBodyBytes({
lineCode: Number(cursor?.serverLastGlobalNumber ?? 0), lineCode: 0,
prevLineNumber: Number(cursor?.serverLastGlobalNumber ?? 0), prevLineNumber: -1,
prevLineHashHex: prevBlockHash, prevLineHashHex: '0'.repeat(64),
thisLineNumber: 1, thisLineNumber: -1,
key: cleanParam, key: cleanParam,
value: cleanValue, value: cleanValue,
}); });
@ -426,7 +435,7 @@ export class AuthService {
return response; return response;
}; };
let cursor = { serverLastGlobalNumber: -1, serverLastGlobalHash: '0'.repeat(64) }; let cursor = freshCursor;
let response = await tryAdd(cursor); let response = await tryAdd(cursor);
if (response.status !== 200) { if (response.status !== 200) {
const knownNum = Number(response?.payload?.serverLastGlobalNumber); const knownNum = Number(response?.payload?.serverLastGlobalNumber);

View File

@ -1,7 +1,7 @@
import { import {
decryptJsonWithStoragePwd, decryptJsonWithStoragePwd,
encryptJsonWithStoragePwd, encryptJsonWithStoragePwd,
} from './crypto-utils.js?v=20260405171816'; } from './crypto-utils.js?v=20260407105357';
const DB_NAME = 'shine-ui-auth'; const DB_NAME = 'shine-ui-auth';
const DB_VERSION = 1; const DB_VERSION = 1;

View File

@ -1,4 +1,4 @@
import { authService, state } from '../state.js?v=20260403081123'; import { authService, state } from '../state.js?v=20260407105357';
export const profileFieldDefs = [ export const profileFieldDefs = [
{ key: 'first_name', readKeys: ['first_name'], label: 'Имя', placeholder: 'Введите имя' }, { key: 'first_name', readKeys: ['first_name'], label: 'Имя', placeholder: 'Введите имя' },

View File

@ -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; const DEFAULT_TIMEOUT_MS = 12000;

View File

@ -1,6 +1,6 @@
import { chatMessages, wallet } from './mock-data.js?v=20260405171816'; import { chatMessages, wallet } from './mock-data.js?v=20260407105357';
import { AuthService } from './services/auth-service.js?v=20260405171816'; import { AuthService } from './services/auth-service.js?v=20260407105357';
import { clearClientAuthData } from './services/key-vault.js?v=20260405171816'; import { clearClientAuthData } from './services/key-vault.js?v=20260407105357';
const clone = (value) => JSON.parse(JSON.stringify(value)); const clone = (value) => JSON.parse(JSON.stringify(value));
const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1'; const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1';

View File

@ -177,6 +177,10 @@
word-break: break-word; word-break: break-word;
} }
.profile-param-value-small {
font-size: 13px;
}
.profile-param-time { .profile-param-time {
font-size: 12px; font-size: 12px;
} }
@ -408,6 +412,10 @@
.avatar { .avatar {
width: 44px; width: 44px;
height: 44px; height: 44px;
min-width: 44px;
min-height: 44px;
flex: 0 0 auto;
aspect-ratio: 1 / 1;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(130deg, #3c4f73, #243352); background: linear-gradient(130deg, #3c4f73, #243352);
display: grid; display: grid;

View File

@ -6,6 +6,7 @@ import blockchain.MsgSubType;
import blockchain.body.BodyHasLine; import blockchain.body.BodyHasLine;
import blockchain.body.BodyHasTarget; import blockchain.body.BodyHasTarget;
import blockchain.body.CreateChannelBody; import blockchain.body.CreateChannelBody;
import blockchain.body.UserParamBody;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws; 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 server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO; import shine.db.dao.BlocksDAO;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.BlockEntry; import shine.db.entities.BlockEntry;
import shine.db.entities.UserParamEntry;
import utils.blockchain.BlockchainNameUtil; import utils.blockchain.BlockchainNameUtil;
import java.util.Arrays; import java.util.Arrays;
@ -45,8 +48,9 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
private final BlocksDAO blocksDAO = BlocksDAO.getInstance(); private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.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 @Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) { 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()); 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) { } catch (Exception e) {
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})", log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",

View File

@ -3,8 +3,10 @@ package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_u
import blockchain.BchBlockEntry; import blockchain.BchBlockEntry;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO; import shine.db.dao.BlocksDAO;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.BlockEntry; import shine.db.entities.BlockEntry;
import shine.db.entities.UserParamEntry;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import java.sql.Connection; import java.sql.Connection;
@ -21,17 +23,20 @@ public final class BlockchainWriter {
private final BlocksDAO blocksDAO; private final BlocksDAO blocksDAO;
private final BlockchainStateDAO stateDAO; private final BlockchainStateDAO stateDAO;
private final UserParamsDAO userParamsDAO;
private final FileStoreUtil fs = FileStoreUtil.getInstance(); private final FileStoreUtil fs = FileStoreUtil.getInstance();
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) { public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO, UserParamsDAO userParamsDAO) {
this.blocksDAO = blocksDAO; this.blocksDAO = blocksDAO;
this.stateDAO = stateDAO; this.stateDAO = stateDAO;
this.userParamsDAO = userParamsDAO;
} }
public void appendBlockAndState(String blockchainName, public void appendBlockAndState(String blockchainName,
BchBlockEntry block, BchBlockEntry block,
BlockchainStateEntry st, BlockchainStateEntry st,
BlockEntry be) throws SQLException { BlockEntry be,
UserParamEntry userParamEntry) throws SQLException {
long nowMs = System.currentTimeMillis(); long nowMs = System.currentTimeMillis();
@ -49,6 +54,11 @@ public final class BlockchainWriter {
stateDAO.upsert(c, st); stateDAO.upsert(c, st);
// 2.1) Если блок USER_PARAM, синхронизируем снимок в users_params в той же транзакции.
if (userParamEntry != null) {
userParamsDAO.upsertIfNewer(c, userParamEntry);
}
c.commit(); c.commit();
} catch (Exception e) { } catch (Exception e) {
try { c.rollback(); } catch (Exception ignored) {} try { c.rollback(); } catch (Exception ignored) {}

View File

@ -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.handlers.tempToTest.entyties.Net_GetUser_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.SolanaUsersDAO; import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry; import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays;
public class Net_GetUser_Handler implements JsonMessageHandler { public class Net_GetUser_Handler implements JsonMessageHandler {
@ -35,6 +38,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
} }
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
try { try {
SolanaUserEntry u = usersDAO.getByLogin(req.getLogin()); SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
@ -60,6 +64,32 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
resp.setBlockchainKey(u.getBlockchainKey()); resp.setBlockchainKey(u.getBlockchainKey());
resp.setDeviceKey(u.getDeviceKey()); 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()); log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
return resp; 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);
}
} }

View File

@ -39,6 +39,8 @@ public class Net_GetUser_Response extends Net_Response {
private String solanaKey; private String solanaKey;
private String blockchainKey; private String blockchainKey;
private String deviceKey; private String deviceKey;
private Integer serverLastGlobalNumber;
private String serverLastGlobalHash;
public Boolean getExists() { return exists; } public Boolean getExists() { return exists; }
public void setExists(Boolean exists) { this.exists = 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 String getDeviceKey() { return deviceKey; }
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
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; }
} }