14-04-2026
Веб пуш работает. Дальше попробую звонки добавить.
This commit is contained in:
parent
21fbc8ffa0
commit
0b7691bdea
@ -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
|
||||
}
|
||||
|
||||
@ -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 = `
|
||||
<button class="ghost-btn" type="button" data-action="refresh">Обновить</button>
|
||||
<button class="ghost-btn" type="button" data-action="copy-all">Скопировать всё</button>
|
||||
<button class="ghost-btn" type="button" data-action="clear">Очистить</button>
|
||||
`;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -289,6 +289,10 @@
|
||||
width: min(100%, 320px);
|
||||
}
|
||||
|
||||
.login-actions-wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-footer-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<JarEntry> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user