Закомитил промежуточную почти работающую версию ...
This commit is contained in:
parent
4deaedf79f
commit
3016d25f73
12
build.gradle
12
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')
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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: '💬' },
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export const profile = {
|
||||
login: '@shine.alex',
|
||||
name: 'Алексей сияющий',
|
||||
name: '',
|
||||
avatarInitials: 'АС',
|
||||
phone: '+7 (916) 221-45-88',
|
||||
address: 'Москва, Пресненская наб., 12',
|
||||
|
||||
@ -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: 'Добавить канал' };
|
||||
|
||||
|
||||
@ -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: 'Канал' };
|
||||
|
||||
|
||||
@ -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: 'Чат' };
|
||||
|
||||
|
||||
@ -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: 'Подключить устройство' };
|
||||
|
||||
|
||||
@ -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: 'Поиск контактов' };
|
||||
|
||||
|
||||
@ -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: 'Подключить через камеру' };
|
||||
|
||||
|
||||
@ -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-код' };
|
||||
|
||||
|
||||
@ -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: 'Сеанс устройства' };
|
||||
|
||||
|
||||
@ -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: 'Устройства' };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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: 'Язык' };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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: 'Личные сообщения' };
|
||||
|
||||
|
||||
@ -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: 'Связи' };
|
||||
|
||||
|
||||
@ -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: 'Уведомления' };
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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: 'Настройки серверов' };
|
||||
|
||||
|
||||
@ -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: 'Настройки' };
|
||||
|
||||
|
||||
@ -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: 'Показать ключи' };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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: 'Кошелёк' };
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: 'Введите имя' },
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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={})",
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user