From 0b7691bdeae80f99a54d7980ae0e5c813eb33813c2547044f0abad8b29d787b0 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 15 Apr 2026 00:50:25 +0300 Subject: [PATCH] =?UTF-8?q?14-04-2026=20=D0=92=D0=B5=D0=B1=20=D0=BF=D1=83?= =?UTF-8?q?=D1=88=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82.=20?= =?UTF-8?q?=D0=94=D0=B0=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BF=D0=BE=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B1=D1=83=D1=8E=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shine-UI/js/app.js | 2 +- shine-UI/js/pages/app-log-view.js | 54 ++++++++++++++++++- shine-UI/js/pages/chat-view.js | 25 ++++++++- shine-UI/js/pages/login-view.js | 2 +- shine-UI/styles/components.css | 4 ++ .../ws_protocol/JSON/push/WebPushSender.java | 24 ++++++++- .../it/IT_DeployRestartAndRunRemoteMain.java | 47 ++++++++++++++++ 7 files changed, 152 insertions(+), 6 deletions(-) diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 5585e92..a159d36 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -292,7 +292,7 @@ async function ensureSessionRuntimeStarted() { pingIntervalId = window.setInterval(async () => { if (!state.session.isAuthorized) return; try { - await authService.ws.request('Ping', { timeMs: Date.now() }); + await authService.ws.request('Ping', { ts: Date.now() }); } catch { // silent keep-alive } diff --git a/shine-UI/js/pages/app-log-view.js b/shine-UI/js/pages/app-log-view.js index b471131..be3b765 100644 --- a/shine-UI/js/pages/app-log-view.js +++ b/shine-UI/js/pages/app-log-view.js @@ -24,10 +24,12 @@ export function render({ navigate }) { const controls = document.createElement('div'); controls.className = 'card row'; - controls.style.justifyContent = 'space-between'; + controls.style.justifyContent = 'flex-start'; controls.style.gap = '8px'; + controls.style.flexWrap = 'wrap'; controls.innerHTML = ` + `; @@ -38,6 +40,39 @@ export function render({ navigate }) { const list = document.createElement('div'); list.className = 'stack'; + function entriesToClipboardText(entries) { + return entries + .slice() + .reverse() + .map((entry) => { + const level = String(entry.level || 'info').toUpperCase(); + const source = String(entry.source || 'ui'); + const header = `[${formatTime(entry.ts)}] ${level} ${source}`; + const message = String(entry.message || ''); + const details = String(entry.details || '').trim(); + return details ? `${header}\n${message}\n${details}` : `${header}\n${message}`; + }) + .join('\n\n'); + } + + async function copyTextToClipboard(text) { + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return true; + } + const ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + ta.style.pointerEvents = 'none'; + document.body.append(ta); + ta.select(); + const ok = document.execCommand('copy'); + ta.remove(); + return !!ok; + } + function renderEntries() { const entries = getAppLogEntries(); list.innerHTML = ''; @@ -85,6 +120,23 @@ export function render({ navigate }) { } controls.querySelector('[data-action="refresh"]').addEventListener('click', renderEntries); + controls.querySelector('[data-action="copy-all"]').addEventListener('click', async () => { + const entries = getAppLogEntries(); + if (!entries.length) { + status.className = 'status-line'; + status.textContent = 'Лог пуст, копировать нечего.'; + return; + } + try { + const text = entriesToClipboardText(entries); + await copyTextToClipboard(text); + status.className = 'status-line is-available'; + status.textContent = `Записей: ${entries.length} · скопировано`; + } catch (e) { + status.className = 'status-line'; + status.textContent = 'Ошибка копирования лога.'; + } + }); controls.querySelector('[data-action="clear"]').addEventListener('click', () => { clearAppLogEntries(); renderEntries(); diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index b1db188..43ab1fb 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'; import { directMessages } from '../mock-data.js'; -import { addChatMessage, getChatMessages, authService, state } from '../state.js'; +import { addAppLogEntry, addChatMessage, getChatMessages, authService, state } from '../state.js'; export const pageMeta = { id: 'chat-view', title: 'Чат' }; @@ -66,14 +66,35 @@ export function render({ navigate, route }) { renderLog(log, chatId); try { - await authService.sendDirectMessage({ + const result = await authService.sendDirectMessage({ login: state.session.login, toLogin: chatId, text, storagePwd: state.session.storagePwdInMemory, }); + addAppLogEntry({ + level: 'info', + source: 'outgoing-dm', + message: `Сообщение отправлено для ${chatId}`, + details: { + toLogin: chatId, + messageId: result?.messageId || '', + deliveredWsSessions: Number(result?.deliveredWsSessions || 0), + deliveredWebPushSessions: Number(result?.deliveredWebPushSessions || 0), + sessionNotFound: Boolean(result?.sessionNotFound), + }, + }); } catch (e) { addChatMessage(chatId, `Ошибка отправки: ${e.message || 'unknown'}`); + addAppLogEntry({ + level: 'warn', + source: 'outgoing-dm', + message: 'Ошибка отправки личного сообщения', + details: { + toLogin: chatId, + error: e?.message || 'unknown', + }, + }); renderLog(log, chatId); } }); diff --git a/shine-UI/js/pages/login-view.js b/shine-UI/js/pages/login-view.js index ab5f68f..0a6c30f 100644 --- a/shine-UI/js/pages/login-view.js +++ b/shine-UI/js/pages/login-view.js @@ -49,7 +49,7 @@ export function render({ navigate }) { loginButton.addEventListener('click', () => navigate('login-password-view')); const actions = document.createElement('div'); - actions.className = 'auth-actions'; + actions.className = 'auth-actions login-actions-wide'; actions.append(cameraButton, loginButton); const backButton = document.createElement('button'); diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 35006c0..59b5ce3 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -289,6 +289,10 @@ width: min(100%, 320px); } +.login-actions-wide { + width: 100%; +} + .auth-footer-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/push/WebPushSender.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/push/WebPushSender.java index 2de3afe..676e34c 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/push/WebPushSender.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/push/WebPushSender.java @@ -10,6 +10,8 @@ import utils.config.AppConfig; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; public final class WebPushSender { private static final Logger log = LoggerFactory.getLogger(WebPushSender.class); @@ -17,10 +19,27 @@ public final class WebPushSender { private WebPushSender() {} + private static void ensureBouncyCastleProvider() { + if (Security.getProvider("BC") != null) return; + try { + Class providerClass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Object providerInstance = providerClass.getDeclaredConstructor().newInstance(); + if (providerInstance instanceof Provider provider) { + Security.addProvider(provider); + log.info("WebPush: registered crypto provider {}", provider.getName()); + return; + } + log.warn("WebPush: class org.bouncycastle.jce.provider.BouncyCastleProvider is not a java.security.Provider"); + } catch (Exception e) { + log.warn("WebPush: failed to register BC provider: {}", e.getMessage()); + } + } + private static PushService service() throws GeneralSecurityException, JoseException { if (service != null) return service; synchronized (WebPushSender.class) { if (service != null) return service; + ensureBouncyCastleProvider(); AppConfig cfg = AppConfig.getInstance(); String pub = cfg.getStringOrEmpty("webpush.vapid.public"); String priv = cfg.getStringOrEmpty("webpush.vapid.private"); @@ -35,6 +54,9 @@ public final class WebPushSender { public static boolean sendBase64Payload(String endpoint, String p256dhKey, String authKey, String payloadB64) { try { + // Some web-push library code may touch crypto provider while building Notification. + // Register BC before creating any push objects. + ensureBouncyCastleProvider(); Subscription subscription = new Subscription( endpoint, new Subscription.Keys(p256dhKey, authKey) @@ -47,7 +69,7 @@ public final class WebPushSender { log.warn("WebPush crypto unsupported", e); return false; } catch (Exception e) { - log.warn("WebPush send failed: {}", e.getMessage()); + log.warn("WebPush send failed: {}", e.getMessage(), e); return false; } } diff --git a/src/test/java/test/it/IT_DeployRestartAndRunRemoteMain.java b/src/test/java/test/it/IT_DeployRestartAndRunRemoteMain.java index de738b0..2ed0afe 100644 --- a/src/test/java/test/it/IT_DeployRestartAndRunRemoteMain.java +++ b/src/test/java/test/it/IT_DeployRestartAndRunRemoteMain.java @@ -2,6 +2,10 @@ package test.it; import test.it.runner.IT_RunAllMain; +import java.io.File; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.Objects; public class IT_DeployRestartAndRunRemoteMain { @@ -30,7 +34,9 @@ public class IT_DeployRestartAndRunRemoteMain { sshStrict("sudo systemctl stop " + SERVICE_NAME + " || true"); // 2) upload jar -> .new + validateLocalFatJarOrThrow(LOCAL_JAR); scpStrict(LOCAL_JAR, REMOTE_JAR + ".new"); + verifyRemoteNewJarOrThrow(REMOTE_JAR + ".new"); // 3) заменить jar атомарно sshStrict("mv -f " + q(REMOTE_JAR + ".new") + " " + q(REMOTE_JAR)); @@ -103,4 +109,45 @@ public class IT_DeployRestartAndRunRemoteMain { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } + + private static void validateLocalFatJarOrThrow(String localJarPath) { + File jar = new File(localJarPath); + if (!jar.isFile()) { + throw new RuntimeException("Local jar not found: " + localJarPath); + } + long size = jar.length(); + // В нашем проекте fat-jar обычно ~30+ MB. Маленький (<10 MB) — почти точно не fat-jar. + if (size < 10L * 1024L * 1024L) { + throw new RuntimeException("Local jar is too small for fat-jar: " + size + " bytes (" + localJarPath + ")"); + } + try (JarFile jf = new JarFile(jar)) { + boolean hasJetty = false; + boolean hasBc = false; + Enumeration entries = jf.entries(); + while (entries.hasMoreElements()) { + String name = entries.nextElement().getName(); + if (!hasJetty && "org/eclipse/jetty/server/Handler.class".equals(name)) hasJetty = true; + if (!hasBc && "org/bouncycastle/jce/provider/BouncyCastleProvider.class".equals(name)) hasBc = true; + if (hasJetty && hasBc) break; + } + if (!hasJetty || !hasBc) { + throw new RuntimeException( + "Local jar doesn't look like fat-jar (missing deps). hasJetty=" + hasJetty + ", hasBC=" + hasBc + ); + } + } catch (Exception e) { + throw new RuntimeException("Failed to inspect local jar: " + localJarPath, e); + } + } + + private static void verifyRemoteNewJarOrThrow(String remoteJarNewPath) { + // Проверка на сервере до mv: файл существует и не подозрительно маленький. + String cmd = "test -f " + q(remoteJarNewPath) + " && " + + "sz=$(stat -c %s " + q(remoteJarNewPath) + ") && " + + "echo remote_new_size=$sz && test \"$sz\" -ge 10485760"; + int code = ssh(cmd); + if (code != 0) { + throw new RuntimeException("Remote uploaded jar is missing or too small: " + remoteJarNewPath); + } + } }