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

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
}
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')
}

View File

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

View File

@ -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() {

View File

@ -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: '💬' },

View File

@ -1,6 +1,6 @@
export const profile = {
login: '@shine.alex',
name: 'Алексей сияющий',
name: '',
avatarInitials: 'АС',
phone: '+7 (916) 221-45-88',
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: 'Добавить канал' };

View File

@ -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: 'Канал' };

View File

@ -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: 'Чат' };

View File

@ -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: 'Подключить устройство' };

View File

@ -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: 'Поиск контактов' };

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: 'Подключить через камеру' };

View File

@ -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-код' };

View File

@ -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: 'Сеанс устройства' };

View File

@ -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: 'Устройства' };

View File

@ -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 };

View File

@ -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 };

View File

@ -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: 'Язык' };

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 };

View File

@ -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 };

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 };

View File

@ -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: 'Личные сообщения' };

View File

@ -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: 'Связи' };

View File

@ -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: 'Уведомления' };

View File

@ -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 }) {
<div class="row" style="gap:12px; align-items:center;">
<div class="avatar large">${profile.avatarInitials}</div>
<div>
<h2 style="font-size:22px; margin-bottom:2px;" data-profile-name="true">${profile.name}</h2>
<p class="meta-muted">${login}</p>
<h2 style="font-size:22px; margin-bottom:2px;" data-profile-login="true">${displayLogin}</h2>
</div>
</div>
<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>
`;
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');
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 = `
<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>
`;
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();

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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: 'Настройки серверов' };

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: 'Настройки' };

View File

@ -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: 'Показать ключи' };

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 };

View File

@ -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 };

View File

@ -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: 'Кошелёк' };

View File

@ -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);

View File

@ -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;

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 = [
{ 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;

View File

@ -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';

View File

@ -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;

View File

@ -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={})",

View File

@ -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) {}

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.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);
}
}

View File

@ -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; }
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; }
}