diff --git a/build.gradle b/build.gradle index ea9d854..7a2abff 100644 --- a/build.gradle +++ b/build.gradle @@ -113,8 +113,36 @@ tasks.named('test') { enabled = false } -tasks.register('itCleanRun', JavaExec) { - group = "build" +tasks.register('cleanServerLogs') { + group = "!!test" + description = "Clear server logs/app.log and remove rolled log files" + + doLast { + File logsDir = file('logs') + if (!logsDir.exists()) { + logsDir.mkdirs() + } + + File appLog = new File(logsDir, 'app.log') + if (!appLog.exists()) { + appLog.createNewFile() + } + appLog.text = '' + + fileTree(logsDir) { + include 'app.*.log' + }.files.each { File f -> + if (!f.delete()) { + throw new GradleException("Failed to delete log file: ${f.absolutePath}") + } + } + + println "Server logs cleared: ${logsDir.absolutePath}" + } +} + +tasks.register('integrationTest', JavaExec) { + group = "!!test" description = "Clean data → kill 7070 → start WS → run all IT tests" classpath = sourceSets.test.runtimeClasspath @@ -127,15 +155,19 @@ tasks.register('itCleanRun', JavaExec) { dependsOn testClasses } -tasks.register('itDeployServer', JavaExec) { - group = "build" +tasks.named('build') { + finalizedBy tasks.named('integrationTest') +} + +tasks.register('deployServer', JavaExec) { + group = "!!deployment" description = "Build → upload to server → clean remote data → restart service → run IT against server" classpath = sourceSets.test.runtimeClasspath mainClass = "test.it.IT_DeployRestartAndRunRemoteMain" // можно переопределить при запуске: - // ./gradlew itDeployServer -Dit.remoteHost=... -Dit.wsUri=... + // ./gradlew deployServer -Dit.remoteHost=... -Dit.wsUri=... dependsOn shadowJar systemProperty "it.remoteHost", System.getProperty("it.remoteHost", "10.147.20.7") systemProperty "it.remoteUser", System.getProperty("it.remoteUser", "user") @@ -149,3 +181,11 @@ tasks.register('itDeployServer', JavaExec) { dependsOn testClasses } + +tasks.register('deployPWA', Exec) { + group = "!!deployment" + description = "Deploy PWA via deploy_shine-PWA.sh" + + workingDir = rootDir + commandLine 'bash', file('deploy_shine-PWA.sh').absolutePath +} diff --git a/deploy_shine-ui.sh b/deploy_shine-PWA.sh similarity index 100% rename from deploy_shine-ui.sh rename to deploy_shine-PWA.sh diff --git a/shine-UI/index.html b/shine-UI/index.html index 8057d1e..8230a04 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -4,9 +4,9 @@ Shine UI Demo - - - + + +
@@ -15,6 +15,6 @@
- + diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 080b679..e27bc2b 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -1,6 +1,7 @@ -import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260330001044'; -import { renderToolbar } from './components/toolbar.js?v=20260330001044'; -import { renderPageLabel } from './components/page-label.js?v=20260330001044'; +import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260330210201'; +import { renderToolbar } from './components/toolbar.js?v=20260330210201'; +import { renderPageLabel } from './components/page-label.js?v=20260330210201'; +import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js?v=20260331000100'; import { authService, authorizeSession, @@ -10,38 +11,38 @@ import { state, terminateCurrentSession, togglePageLabel, -} from './state.js?v=20260330001044'; +} from './state.js?v=20260330210201'; -import * as startView from './pages/start-view.js?v=20260330001044'; -import * as entrySettingsView from './pages/entry-settings-view.js?v=20260330001044'; -import * as registerView from './pages/register-view.js?v=20260330001044'; -import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260330001044'; -import * as registrationKeysView from './pages/registration-keys-view.js?v=20260330001044'; -import * as topupView from './pages/topup-view.js?v=20260330001044'; -import * as loginView from './pages/login-view.js?v=20260330001044'; -import * as loginCameraView from './pages/login-camera-view.js?v=20260330001044'; -import * as loginPasswordView from './pages/login-password-view.js?v=20260330001044'; -import * as keyStorageView from './pages/key-storage-view.js?v=20260330001044'; +import * as startView from './pages/start-view.js?v=20260330210201'; +import * as entrySettingsView from './pages/entry-settings-view.js?v=20260330210201'; +import * as registerView from './pages/register-view.js?v=20260330210201'; +import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260330210201'; +import * as registrationKeysView from './pages/registration-keys-view.js?v=20260330210201'; +import * as topupView from './pages/topup-view.js?v=20260330210201'; +import * as loginView from './pages/login-view.js?v=20260330210201'; +import * as loginCameraView from './pages/login-camera-view.js?v=20260330210201'; +import * as loginPasswordView from './pages/login-password-view.js?v=20260330210201'; +import * as keyStorageView from './pages/key-storage-view.js?v=20260330210201'; -import * as profileView from './pages/profile-view.js?v=20260330001044'; -import * as walletView from './pages/wallet-view.js?v=20260330001044'; -import * as settingsView from './pages/settings-view.js?v=20260330001044'; -import * as serverSettingsView from './pages/server-settings-view.js?v=20260330001044'; -import * as deviceView from './pages/device-view.js?v=20260330001044'; -import * as connectDeviceView from './pages/connect-device-view.js?v=20260330001044'; -import * as deviceQrView from './pages/device-qr-view.js?v=20260330001044'; -import * as deviceCameraView from './pages/device-camera-view.js?v=20260330001044'; -import * as showKeysView from './pages/show-keys-view.js?v=20260330001044'; -import * as deviceSessionView from './pages/device-session-view.js?v=20260330001044'; -import * as languageView from './pages/language-view.js?v=20260330001044'; -import * as messagesList from './pages/messages-list.js?v=20260330001044'; -import * as contactSearchView from './pages/contact-search-view.js?v=20260330001044'; -import * as chatView from './pages/chat-view.js?v=20260330001044'; -import * as channelsList from './pages/channels-list.js?v=20260330001044'; -import * as channelView from './pages/channel-view.js?v=20260330001044'; -import * as addChannelView from './pages/add-channel-view.js?v=20260330001044'; -import * as networkView from './pages/network-view.js?v=20260330001044'; -import * as notificationsView from './pages/notifications-view.js?v=20260330001044'; +import * as profileView from './pages/profile-view.js?v=20260330210201'; +import * as walletView from './pages/wallet-view.js?v=20260330210201'; +import * as settingsView from './pages/settings-view.js?v=20260330210201'; +import * as serverSettingsView from './pages/server-settings-view.js?v=20260330210201'; +import * as deviceView from './pages/device-view.js?v=20260330210201'; +import * as connectDeviceView from './pages/connect-device-view.js?v=20260330210201'; +import * as deviceQrView from './pages/device-qr-view.js?v=20260330210201'; +import * as deviceCameraView from './pages/device-camera-view.js?v=20260330210201'; +import * as showKeysView from './pages/show-keys-view.js?v=20260330210201'; +import * as deviceSessionView from './pages/device-session-view.js?v=20260330210201'; +import * as languageView from './pages/language-view.js?v=20260330210201'; +import * as messagesList from './pages/messages-list.js?v=20260330210201'; +import * as contactSearchView from './pages/contact-search-view.js?v=20260330210201'; +import * as chatView from './pages/chat-view.js?v=20260330210201'; +import * as channelsList from './pages/channels-list.js?v=20260330210201'; +import * as channelView from './pages/channel-view.js?v=20260330210201'; +import * as addChannelView from './pages/add-channel-view.js?v=20260330210201'; +import * as networkView from './pages/network-view.js?v=20260330210201'; +import * as notificationsView from './pages/notifications-view.js?v=20260330210201'; const routes = { 'start-view': startView, @@ -81,6 +82,35 @@ const toolbarEl = document.getElementById('toolbar-slot'); let currentCleanup = null; +setClientErrorTransport((payload) => authService.reportClientError(payload)); + +window.addEventListener('error', (event) => { + captureClientError({ + kind: 'global_error', + message: event.message || 'Global JS error', + stack: event.error?.stack || '', + sourceUrl: event.filename || '', + lineNumber: event.lineno, + columnNumber: event.colno, + context: { + pageId: getRoute().pageId || '', + }, + }); +}); + +window.addEventListener('unhandledrejection', (event) => { + const reason = event.reason; + captureClientError({ + kind: 'unhandled_rejection', + message: reason?.message || String(reason || 'Unhandled promise rejection'), + stack: reason?.stack || '', + context: { + pageId: getRoute().pageId || '', + reasonType: reason?.constructor?.name || typeof reason, + }, + }); +}); + function renderApp() { const route = getRoute(); const pageId = route.pageId || (state.session.isAuthorized ? 'profile-view' : 'start-view'); diff --git a/shine-UI/js/components/toolbar.js b/shine-UI/js/components/toolbar.js index 7aa2dda..756979b 100644 --- a/shine-UI/js/components/toolbar.js +++ b/shine-UI/js/components/toolbar.js @@ -1,4 +1,4 @@ -import { resolveToolbarActive } from '../router.js?v=20260330001044'; +import { resolveToolbarActive } from '../router.js?v=20260330210201'; const ITEMS = [ { pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' }, diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js index a9238f9..e332d9f 100644 --- a/shine-UI/js/pages/add-channel-view.js +++ b/shine-UI/js/pages/add-channel-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; export const pageMeta = { id: 'add-channel-view', title: 'Добавить канал' }; diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index ab315e2..7185017 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { channelPosts, channels } from '../mock-data.js?v=20260330001044'; -import { authService, state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { channelPosts, channels } from '../mock-data.js?v=20260330210201'; +import { addLocalChannelPost, authService, getLocalChannelPosts, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'channel-view', title: 'Канал' }; @@ -8,7 +8,10 @@ function findMockChannel(channelId) { const channel = channels.find((c) => c.id === channelId) || channels[0]; return { channel, - posts: (channelPosts[channel.id] || []).map((post) => ({ title: post.title, body: post.body })), + posts: [ + ...(channelPosts[channel.id] || []).map((post) => ({ title: post.title, body: post.body })), + ...getLocalChannelPosts(channelId), + ], isOwnChannel: channel.ownerLogin === '@shine.alex', }; } @@ -20,7 +23,55 @@ function mapApiMessageToPost(message) { }; } -function renderBody(screen, navigate, channelData) { +function renderPostCard(post) { + const card = document.createElement('article'); + card.className = 'card stack'; + card.innerHTML = `${post.title}

${post.body}

`; + return card; +} + +function openAddMessageModal({ channelId, channelName, onSubmit }) { + const root = document.getElementById('modal-root'); + root.innerHTML = ` + + `; + + const textEl = root.querySelector('#channel-message-text'); + const errorEl = root.querySelector('#channel-message-error'); + const close = () => { + root.innerHTML = ''; + }; + + root.querySelector('#channel-message-cancel').addEventListener('click', close); + root.querySelector('#channel-message-submit').addEventListener('click', () => { + const body = textEl.value.trim(); + if (!body) { + errorEl.textContent = 'Введите текст сообщения.'; + return; + } + + onSubmit({ + title: `${state.session.login || 'Вы'} • сейчас`, + body, + }); + close(); + }); + + if (textEl) textEl.focus(); +} + +function renderBody(screen, navigate, channelId, channelData) { const head = document.createElement('div'); head.className = 'card'; head.innerHTML = ` @@ -37,12 +88,23 @@ function renderBody(screen, navigate, channelData) { feed.className = 'stack'; channelData.posts.forEach((post) => { - const card = document.createElement('article'); - card.className = 'card stack'; - card.innerHTML = `${post.title}

${post.body}

`; - feed.append(card); + feed.append(renderPostCard(post)); }); + if (channelData.isOwnChannel) { + actionButton.addEventListener('click', () => { + openAddMessageModal({ + channelId, + channelName: channelData.channel.name, + onSubmit: (post) => { + addLocalChannelPost(channelId, post); + channelData.posts.push(post); + feed.append(renderPostCard(post)); + }, + }); + }); + } + const backButton = document.createElement('button'); backButton.className = 'secondary-btn'; backButton.textContent = 'Назад к списку'; @@ -64,7 +126,10 @@ async function loadFromApi(channelId) { if (!selector.ownerBlockchainName || selector.channelRootBlockNumber == null) return null; const payload = await authService.getChannelMessages(selector, 200, 'asc'); - const posts = (payload.messages || []).map(mapApiMessageToPost); + const posts = [ + ...(payload.messages || []).map(mapApiMessageToPost), + ...getLocalChannelPosts(channelId), + ]; return { channel: { @@ -104,7 +169,7 @@ export function render({ navigate, route }) { const apiData = await loadFromApi(channelId); loading.remove(); if (apiData) { - renderBody(screen, navigate, apiData); + renderBody(screen, navigate, channelId, apiData); return; } } catch { @@ -112,7 +177,7 @@ export function render({ navigate, route }) { } loading.remove(); - renderBody(screen, navigate, findMockChannel(channelId)); + renderBody(screen, navigate, channelId, findMockChannel(channelId)); })(); return screen; diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index ed39609..bde9747 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { channels as mockChannels } from '../mock-data.js?v=20260330001044'; -import { authService, setChannelsFeed, state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { channels as mockChannels } from '../mock-data.js?v=20260330210201'; +import { authService, setChannelsFeed, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'channels-list', title: 'Каналы' }; diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 5f77b7b..60dbbbf 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { directMessages } from '../mock-data.js?v=20260330001044'; -import { addChatMessage, getChatMessages } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { directMessages } from '../mock-data.js?v=20260330210201'; +import { addChatMessage, getChatMessages } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'chat-view', title: 'Чат' }; diff --git a/shine-UI/js/pages/connect-device-view.js b/shine-UI/js/pages/connect-device-view.js index d560ca2..c45f9fb 100644 --- a/shine-UI/js/pages/connect-device-view.js +++ b/shine-UI/js/pages/connect-device-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'connect-device-view', title: 'Подключить устройство' }; diff --git a/shine-UI/js/pages/contact-search-view.js b/shine-UI/js/pages/contact-search-view.js index 5b1f695..a64e902 100644 --- a/shine-UI/js/pages/contact-search-view.js +++ b/shine-UI/js/pages/contact-search-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { contactDirectory, directMessages } from '../mock-data.js?v=20260330001044'; -import { ensureChat } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { contactDirectory, directMessages } from '../mock-data.js?v=20260330210201'; +import { ensureChat } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' }; diff --git a/shine-UI/js/pages/device-camera-view.js b/shine-UI/js/pages/device-camera-view.js index 57a7f76..905b191 100644 --- a/shine-UI/js/pages/device-camera-view.js +++ b/shine-UI/js/pages/device-camera-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; export const pageMeta = { id: 'device-camera-view', title: 'Подключить через камеру' }; diff --git a/shine-UI/js/pages/device-qr-view.js b/shine-UI/js/pages/device-qr-view.js index f054ac1..e1683ba 100644 --- a/shine-UI/js/pages/device-qr-view.js +++ b/shine-UI/js/pages/device-qr-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { profile } from '../mock-data.js?v=20260330001044'; -import { state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { profile } from '../mock-data.js?v=20260330210201'; +import { state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'device-qr-view', title: 'Показать QR-код' }; diff --git a/shine-UI/js/pages/device-session-view.js b/shine-UI/js/pages/device-session-view.js index ff6d51c..2ecacb7 100644 --- a/shine-UI/js/pages/device-session-view.js +++ b/shine-UI/js/pages/device-session-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; import { authService, isSessionInvalidError, @@ -6,7 +6,7 @@ import { setAuthError, state, terminateCurrentSession, -} from '../state.js?v=20260330001044'; +} from '../state.js?v=20260330210201'; export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' }; diff --git a/shine-UI/js/pages/device-view.js b/shine-UI/js/pages/device-view.js index 9d82ffe..c02719d 100644 --- a/shine-UI/js/pages/device-view.js +++ b/shine-UI/js/pages/device-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; import { authService, isSessionInvalidError, @@ -7,7 +7,7 @@ import { setAuthInfo, state, terminateCurrentSession, -} from '../state.js?v=20260330001044'; +} from '../state.js?v=20260330210201'; export const pageMeta = { id: 'device-view', title: 'Устройства' }; diff --git a/shine-UI/js/pages/entry-settings-view.js b/shine-UI/js/pages/entry-settings-view.js index ce5394e..7b378b2 100644 --- a/shine-UI/js/pages/entry-settings-view.js +++ b/shine-UI/js/pages/entry-settings-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false }; diff --git a/shine-UI/js/pages/key-storage-view.js b/shine-UI/js/pages/key-storage-view.js index 784b2e8..6db4f7d 100644 --- a/shine-UI/js/pages/key-storage-view.js +++ b/shine-UI/js/pages/key-storage-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { authorizeSession, state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { authorizeSession, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false }; diff --git a/shine-UI/js/pages/language-view.js b/shine-UI/js/pages/language-view.js index cdc53cf..85775bf 100644 --- a/shine-UI/js/pages/language-view.js +++ b/shine-UI/js/pages/language-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'language-view', title: 'Язык' }; diff --git a/shine-UI/js/pages/login-camera-view.js b/shine-UI/js/pages/login-camera-view.js index ba09bae..8367fba 100644 --- a/shine-UI/js/pages/login-camera-view.js +++ b/shine-UI/js/pages/login-camera-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; export const pageMeta = { id: 'login-camera-view', title: 'Войти по камере', showAppChrome: false }; diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index 3af3451..35bf872 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -1,11 +1,11 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; import { authService, clearAuthMessages, setAuthBusy, setAuthError, state, -} from '../state.js?v=20260330001044'; +} from '../state.js?v=20260330210201'; export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false }; diff --git a/shine-UI/js/pages/login-view.js b/shine-UI/js/pages/login-view.js index e152d41..e9ba70c 100644 --- a/shine-UI/js/pages/login-view.js +++ b/shine-UI/js/pages/login-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; export const pageMeta = { id: 'login-view', title: 'Войти', showAppChrome: false }; diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index 837a7dc..53d159e 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { directMessages } from '../mock-data.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { directMessages } from '../mock-data.js?v=20260330210201'; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index 0418844..b763c18 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { networkGraph } from '../mock-data.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { networkGraph } from '../mock-data.js?v=20260330210201'; export const pageMeta = { id: 'network-view', title: 'Связи' }; diff --git a/shine-UI/js/pages/notifications-view.js b/shine-UI/js/pages/notifications-view.js index 33cee7c..c168748 100644 --- a/shine-UI/js/pages/notifications-view.js +++ b/shine-UI/js/pages/notifications-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { notifications } from '../mock-data.js?v=20260330001044'; -import { state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { notifications } from '../mock-data.js?v=20260330210201'; +import { state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'notifications-view', title: 'Уведомления' }; diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js index 6fc256f..a6bc575 100644 --- a/shine-UI/js/pages/profile-view.js +++ b/shine-UI/js/pages/profile-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { profile } from '../mock-data.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { profile } from '../mock-data.js?v=20260330210201'; export const pageMeta = { id: 'profile-view', title: 'Профиль' }; diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index a252081..41ec606 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { authService, clearAuthMessages, state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { authService, clearAuthMessages, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; diff --git a/shine-UI/js/pages/registration-keys-view.js b/shine-UI/js/pages/registration-keys-view.js index b55f1eb..8e318a7 100644 --- a/shine-UI/js/pages/registration-keys-view.js +++ b/shine-UI/js/pages/registration-keys-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; import { authService, authorizeSession, @@ -6,7 +6,7 @@ import { setAuthError, setAuthInfo, state, -} from '../state.js?v=20260330001044'; +} from '../state.js?v=20260330210201'; export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false }; diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index bb4a3ac..65a01da 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -1,11 +1,11 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; import { authService, refreshRegistrationBalance, setAuthError, setAuthInfo, state, -} from '../state.js?v=20260330001044'; +} from '../state.js?v=20260330210201'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; diff --git a/shine-UI/js/pages/server-settings-view.js b/shine-UI/js/pages/server-settings-view.js index 0101e34..634e4d5 100644 --- a/shine-UI/js/pages/server-settings-view.js +++ b/shine-UI/js/pages/server-settings-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' }; diff --git a/shine-UI/js/pages/settings-view.js b/shine-UI/js/pages/settings-view.js index 41f59e2..e65e99f 100644 --- a/shine-UI/js/pages/settings-view.js +++ b/shine-UI/js/pages/settings-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; export const pageMeta = { id: 'settings-view', title: 'Настройки' }; diff --git a/shine-UI/js/pages/show-keys-view.js b/shine-UI/js/pages/show-keys-view.js index fd9fb16..eb7d11c 100644 --- a/shine-UI/js/pages/show-keys-view.js +++ b/shine-UI/js/pages/show-keys-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { state } from '../state.js?v=20260330001044'; -import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { state } from '../state.js?v=20260330210201'; +import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260330210201'; export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' }; diff --git a/shine-UI/js/pages/start-view.js b/shine-UI/js/pages/start-view.js index 15f9f22..4967be4 100644 --- a/shine-UI/js/pages/start-view.js +++ b/shine-UI/js/pages/start-view.js @@ -1,4 +1,4 @@ -import { clearStartHint, state } from '../state.js?v=20260330001044'; +import { clearStartHint, state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'start-view', title: 'Старт', showAppChrome: false }; diff --git a/shine-UI/js/pages/topup-view.js b/shine-UI/js/pages/topup-view.js index 7dd9f74..8ffacd0 100644 --- a/shine-UI/js/pages/topup-view.js +++ b/shine-UI/js/pages/topup-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { state } from '../state.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { state } from '../state.js?v=20260330210201'; export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false }; diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index ab9b0a7..8aae30e 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260330001044'; -import { wallet } from '../mock-data.js?v=20260330001044'; +import { renderHeader } from '../components/header.js?v=20260330210201'; +import { wallet } from '../mock-data.js?v=20260330210201'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 8b0f8c3..24e94e2 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -1,4 +1,4 @@ -import { WsJsonClient } from './ws-client.js?v=20260330001044'; +import { WsJsonClient } from './ws-client.js?v=20260330210201'; import { deriveEd25519FromPassword, exportEd25519PublicKeyB64, @@ -7,8 +7,8 @@ import { importPkcs8Ed25519, randomBase64, signBase64, -} from './crypto-utils.js?v=20260330001044'; -import { loadSessionMaterial, saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260330001044'; +} from './crypto-utils.js?v=20260330210201'; +import { loadSessionMaterial, saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260330210201'; const BCH_SUFFIX = '001'; @@ -235,6 +235,15 @@ export class AuthService { return response.payload || {}; } + async reportClientError(details) { + try { + const response = await this.ws.request('ClientErrorLog', details || {}, 3000); + return response?.status === 200; + } catch { + return false; + } + } + close() { this.ws.close(); } diff --git a/shine-UI/js/services/key-vault.js b/shine-UI/js/services/key-vault.js index e988a28..6db0af3 100644 --- a/shine-UI/js/services/key-vault.js +++ b/shine-UI/js/services/key-vault.js @@ -1,7 +1,7 @@ import { decryptJsonWithStoragePwd, encryptJsonWithStoragePwd, -} from './crypto-utils.js?v=20260330001044'; +} from './crypto-utils.js?v=20260330210201'; const DB_NAME = 'shine-ui-auth'; const DB_VERSION = 1; diff --git a/shine-UI/js/services/ws-client.js b/shine-UI/js/services/ws-client.js index ff7d06b..39b2f8b 100644 --- a/shine-UI/js/services/ws-client.js +++ b/shine-UI/js/services/ws-client.js @@ -1,3 +1,5 @@ +import { captureClientError } from './client-error-reporter.js?v=20260331000100'; + const DEFAULT_TIMEOUT_MS = 12000; function buildWsUrl(raw) { @@ -34,6 +36,11 @@ export class WsJsonClient { }, { once: true }); ws.addEventListener('error', () => { + captureClientError({ + kind: 'ws_open_error', + message: `Failed to connect WebSocket ${this.url}`, + context: { url: this.url }, + }); reject(new Error(`Не удалось подключиться к ${this.url}`)); }, { once: true }); @@ -59,10 +66,20 @@ export class WsJsonClient { const responsePromise = new Promise((resolve, reject) => { const timer = window.setTimeout(() => { this.pending.delete(requestId); + if (op !== 'ClientErrorLog') { + captureClientError({ + kind: 'ws_timeout', + message: `Timeout waiting for ${op}`, + requestOp: op, + requestIdRef: requestId, + context: { url: this.url, timeoutMs }, + }); + } reject(new Error(`Таймаут ответа для операции ${op}`)); }, timeoutMs); this.pending.set(requestId, { + op, resolve: (value) => { window.clearTimeout(timer); resolve(value); @@ -90,6 +107,11 @@ export class WsJsonClient { try { data = JSON.parse(raw); } catch { + captureClientError({ + kind: 'ws_bad_json', + message: 'Received non-JSON message from server', + context: { raw: String(raw).slice(0, 1000) }, + }); return; } @@ -103,6 +125,17 @@ export class WsJsonClient { } failPending(message) { + const pendingOps = [...this.pending.values()] + .map((slot) => slot.op) + .filter((op) => op && op !== 'ClientErrorLog'); + if (pendingOps.length > 0) { + captureClientError({ + kind: 'ws_closed', + message, + context: { url: this.url, pendingOps }, + }); + } + const error = new Error(message); for (const [, slot] of this.pending.entries()) { slot.reject(error); diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js index 268e6c6..a72df68 100644 --- a/shine-UI/js/state.js +++ b/shine-UI/js/state.js @@ -1,6 +1,6 @@ -import { chatMessages, wallet } from './mock-data.js?v=20260330001044'; -import { AuthService } from './services/auth-service.js?v=20260330001044'; -import { clearClientAuthData } from './services/key-vault.js?v=20260330001044'; +import { chatMessages, wallet } from './mock-data.js?v=20260330210201'; +import { AuthService } from './services/auth-service.js?v=20260330210201'; +import { clearClientAuthData } from './services/key-vault.js?v=20260330210201'; const clone = (value) => JSON.parse(JSON.stringify(value)); const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1'; @@ -99,6 +99,7 @@ function createInitialState({ withStoredSession = true } = {}) { sessions: [], channelsFeed: null, channelsIndex: {}, + localChannelPosts: {}, }; } @@ -239,3 +240,22 @@ export function setChannelsFeed(feed, index) { state.channelsFeed = feed || null; state.channelsIndex = index || {}; } + +export function getLocalChannelPosts(channelId) { + if (!channelId) return []; + if (!state.localChannelPosts[channelId]) { + state.localChannelPosts[channelId] = []; + } + return state.localChannelPosts[channelId]; +} + +export function addLocalChannelPost(channelId, post) { + if (!channelId) return; + const text = post?.body?.trim(); + if (!text) return; + + getLocalChannelPosts(channelId).push({ + title: post.title || `${state.session.login || 'Вы'} • сейчас`, + body: text, + }); +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index b39278c..3d41a40 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -54,7 +54,9 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscrip // --- NEW: Ping --- import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler; +import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request; @@ -97,7 +99,8 @@ public final class JsonHandlerRegistry { // --- system --- Map.entry("Ping", new Net_Ping_Handler()), - Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()) + Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()), + Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()) // --- subscriptions --- // Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler()) @@ -134,7 +137,8 @@ public final class JsonHandlerRegistry { // --- system --- Map.entry("Ping", Net_Ping_Request.class), - Map.entry("GetServerInfo", Net_GetServerInfo_Request.class) + Map.entry("GetServerInfo", Net_GetServerInfo_Request.class), + Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class) ); private JsonHandlerRegistry() { } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java index 6d266bc..1b1f49d 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java @@ -121,7 +121,7 @@ final class ChannelsReadSupport { try { BchBlockEntry e = new BchBlockEntry(blockBytes); TextInfo ti = new TextInfo(); - ti.createdAtMs = e.timeMs; + ti.createdAtMs = e.timestamp * 1000L; if (e.body instanceof TextBody tb) { ti.text = tb.message; } @@ -137,7 +137,8 @@ final class ChannelsReadSupport { SELECT login,bch_name,block_number,block_hash,block_bytes FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type=? AND line_code=? - ORDER BY block_number """ + order + " LIMIT ?"; + ORDER BY block_number + """ + order + " LIMIT ?"; try (PreparedStatement ps = c.prepareStatement(sql)) { ps.setString(1, ownerBch); ps.setInt(2, MSG_TYPE_TEXT);