Добавить деплой без clean/тестов и доработки PWA/сессии в UI

This commit is contained in:
AidarKC
2026-04-20 20:33:20 +03:00
parent bec1d08757
commit 2d48ae7a16
8 changed files with 404 additions and 4 deletions
+2
View File
@@ -1,6 +1,7 @@
import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js';
import { renderToolbar } from './components/toolbar.js';
import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js';
import { initPwaInstallPromptHandling } from './services/pwa-install-service.js';
import { initPwaPush } from './services/pwa-push-service.js';
import { handleIncomingCallInvite, handleIncomingCallSignal } from './services/call-service.js';
import {
@@ -96,6 +97,7 @@ let pingIntervalId = null;
let sessionRuntimeStarted = false;
setClientErrorTransport((payload) => authService.reportClientError(payload));
initPwaInstallPromptHandling();
function showGlobalErrorAlert(title, details = {}) {
const lines = [title];
+93
View File
@@ -1,4 +1,12 @@
import { renderHeader } from '../components/header.js';
import { addAppLogEntry, authService, closeCurrentSessionAndSignOut, state } from '../state.js';
import {
canInstallPwa,
isStandalonePwaMode,
onPwaInstallAvailabilityChange,
promptPwaInstall,
} from '../services/pwa-install-service.js';
import { initPwaPush } from '../services/pwa-push-service.js';
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
@@ -20,6 +28,8 @@ export function render({ navigate }) {
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
<button class="text-btn" type="button" id="settings-pwa-install">Зарегистрировать PWA</button>
`;
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
@@ -27,6 +37,89 @@ export function render({ navigate }) {
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
card.querySelector('#settings-app-log').addEventListener('click', () => navigate('app-log-view'));
const signOutBtn = card.querySelector('#settings-signout');
const pwaInstallBtn = card.querySelector('#settings-pwa-install');
const syncPwaButtonLabel = () => {
if (isStandalonePwaMode()) {
pwaInstallBtn.textContent = 'PWA установлено (проверить WebPush)';
return;
}
if (canInstallPwa()) {
pwaInstallBtn.textContent = 'Зарегистрировать PWA';
return;
}
pwaInstallBtn.textContent = 'Как установить PWA';
};
const unsubscribeInstallAvailability = onPwaInstallAvailabilityChange(() => {
syncPwaButtonLabel();
});
syncPwaButtonLabel();
signOutBtn.addEventListener('click', async () => {
const confirmed = window.confirm(
'Завершить текущую сессию на сервере, отключиться, очистить локальные данные и перейти на стартовый экран?'
);
if (!confirmed) return;
signOutBtn.disabled = true;
try {
addAppLogEntry({
level: 'info',
source: 'session',
message: `Запрошено завершение текущей сессии: ${state.session.sessionId || 'unknown'}`,
});
await closeCurrentSessionAndSignOut({
infoMessage: 'Сеанс завершён. Выполните вход заново.',
});
} finally {
signOutBtn.disabled = false;
}
});
pwaInstallBtn.addEventListener('click', async () => {
pwaInstallBtn.disabled = true;
try {
await initPwaPush({
authService,
onLog: (entry) => addAppLogEntry(entry),
});
if (canInstallPwa()) {
const result = await promptPwaInstall();
const accepted = result.outcome === 'accepted';
addAppLogEntry({
level: 'info',
source: 'pwa-install',
message: accepted ? 'Пользователь принял установку PWA' : 'Пользователь отклонил установку PWA',
details: { outcome: result.outcome || 'unknown' },
});
if (accepted) {
window.alert('Установка PWA подтверждена. Проверьте приложение на главном экране устройства.');
}
} else if (!isStandalonePwaMode()) {
window.alert('Для установки откройте меню браузера и выберите "Установить приложение" или "Добавить на главный экран".');
} else {
window.alert('PWA уже установлено. WebPush перерегистрирован.');
}
} catch (error) {
addAppLogEntry({
level: 'warn',
source: 'pwa-install',
message: 'Не удалось зарегистрировать PWA/WebPush',
details: { error: error?.message || 'unknown' },
});
window.alert(`Ошибка регистрации PWA: ${error?.message || 'unknown'}`);
} finally {
pwaInstallBtn.disabled = false;
syncPwaButtonLabel();
}
});
screen.append(card);
screen.cleanup = () => {
unsubscribeInstallAvailability();
};
return screen;
}
+9
View File
@@ -48,3 +48,12 @@ export async function listStoredMessages() {
req.onerror = () => reject(req.error || new Error('IndexedDB getAll failed'));
}));
}
export async function clearStoredMessages() {
await new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(DB_NAME);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error || new Error('IndexedDB delete failed'));
request.onblocked = () => reject(new Error('IndexedDB delete blocked'));
});
}
@@ -0,0 +1,66 @@
let initialized = false;
let deferredInstallPrompt = null;
const listeners = new Set();
function notifyAvailability() {
const canInstall = Boolean(deferredInstallPrompt);
listeners.forEach((listener) => {
try {
listener(canInstall);
} catch {
// ignore listener errors
}
});
}
export function initPwaInstallPromptHandling() {
if (initialized) return;
initialized = true;
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredInstallPrompt = event;
notifyAvailability();
});
window.addEventListener('appinstalled', () => {
deferredInstallPrompt = null;
notifyAvailability();
});
}
export function canInstallPwa() {
return Boolean(deferredInstallPrompt);
}
export function isStandalonePwaMode() {
return (
window.matchMedia?.('(display-mode: standalone)')?.matches ||
window.navigator?.standalone === true
);
}
export async function promptPwaInstall() {
if (!deferredInstallPrompt) {
return { supported: false, outcome: '' };
}
const promptEvent = deferredInstallPrompt;
deferredInstallPrompt = null;
notifyAvailability();
await promptEvent.prompt();
const choice = await promptEvent.userChoice;
return {
supported: true,
outcome: String(choice?.outcome || ''),
};
}
export function onPwaInstallAvailabilityChange(handler) {
if (typeof handler !== 'function') return () => {};
listeners.add(handler);
return () => {
listeners.delete(handler);
};
}
+65 -4
View File
@@ -1,11 +1,15 @@
import { chatMessages, wallet } from './mock-data.js';
import { wallet } from './mock-data.js';
import { AuthService } from './services/auth-service.js';
import { clearClientAuthData } from './services/key-vault.js';
import { listStoredMessages, putStoredMessage } from './services/message-store.js';
import { clearStoredMessages, listStoredMessages, putStoredMessage } from './services/message-store.js';
const clone = (value) => JSON.parse(JSON.stringify(value));
const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1';
const REACTIONS_STORAGE_KEY = 'shine-ui-message-reactions-v2';
const WEB_PUSH_SUBSCRIPTION_KEY = 'shine-ui-webpush-subscription-v1';
const CHANNEL_NOTIFY_KEY = 'shine-channels-notify-v1';
const CHANNELS_DEMO_KEY = 'shine-channels-demo';
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
const MAX_APP_LOG_ENTRIES = 500;
const INVALID_SESSION_CODES = new Set([
'NOT_AUTHENTICATED',
@@ -121,13 +125,36 @@ function clearStoredSession() {
}
}
function clearBrowserClientData() {
const localKeys = [
SESSION_STORAGE_KEY,
REACTIONS_STORAGE_KEY,
WEB_PUSH_SUBSCRIPTION_KEY,
CHANNEL_NOTIFY_KEY,
CHANNELS_DEMO_KEY,
];
localKeys.forEach((key) => {
try {
localStorage.removeItem(key);
} catch {
// ignore
}
});
try {
sessionStorage.removeItem(CREATE_CHANNEL_FLASH_KEY);
} catch {
// ignore
}
}
function createInitialState({ withStoredSession = true } = {}) {
const storedSession = withStoredSession ? loadStoredSession() : null;
const storedReactions = loadStoredReactions();
const initialShineServer = LOCAL_WS_OVERRIDE_URL || DEFAULT_SHINE_SERVER;
return {
chats: clone(chatMessages),
chats: {},
contacts: [],
appLog: [],
incomingDedup: {},
@@ -504,9 +531,16 @@ export async function refreshSessions() {
function resetStateForSignedOut() {
const next = createInitialState({ withStoredSession: false });
state.chats = next.chats;
state.contacts = next.contacts;
state.appLog = next.appLog;
state.incomingDedup = next.incomingDedup;
state.knownMessageKeys = next.knownMessageKeys;
state.outgoingTempSeq = next.outgoingTempSeq;
state.notificationsTab = next.notificationsTab;
state.pageLabelCollapsed = next.pageLabelCollapsed;
state.session = next.session;
state.startHint = next.startHint;
state.entrySettings = next.entrySettings;
state.registrationDraft = next.registrationDraft;
state.loginDraft = next.loginDraft;
state.registrationPayment = next.registrationPayment;
@@ -522,21 +556,48 @@ function resetStateForSignedOut() {
export async function terminateCurrentSession({ infoMessage = '' } = {}) {
clearStoredSession();
clearBrowserClientData();
resetStateForSignedOut();
authService.close();
try {
await clearClientAuthData();
await Promise.all([
clearClientAuthData(),
clearStoredMessages(),
]);
} catch {
// ignore cleanup errors in prototype mode
}
if (infoMessage) {
state.startHint = infoMessage;
}
try {
await authService.reconnect(state.entrySettings.shineServer);
} catch {
// ignore reconnect errors on sign out
}
if (onSessionReset) {
onSessionReset();
}
}
export async function closeCurrentSessionAndSignOut({ infoMessage = '' } = {}) {
const currentSessionId = String(state.session.sessionId || '').trim();
try {
if (state.session.isAuthorized && currentSessionId) {
await authService.closeSession(currentSessionId);
}
} catch (error) {
addAppLogEntry({
level: 'warn',
source: 'session',
message: 'Не удалось завершить текущую сессию на сервере',
details: { sessionId: currentSessionId, error: error?.message || 'unknown' },
});
}
await terminateCurrentSession({ infoMessage });
}
export function refreshRegistrationBalance() {
const next = (0.005 + Math.random() * 0.03).toFixed(4);
state.registrationPayment.balanceSOL = next;