SHA256
405 lines
18 KiB
HTML
405 lines
18 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-visual"
|
|
/>
|
|
<base href="/" />
|
|
<link rel="manifest" href="./manifest.webmanifest" />
|
|
<link rel="icon" type="image/jpeg" href="./img/logo.jpg" />
|
|
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
|
<title>СИЯНИЕ</title>
|
|
<script>
|
|
window.__SHINE_BUILD_HASH__ = '20260826145335';
|
|
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
|
</script>
|
|
<script>
|
|
(function attachStylesWithBuildHash() {
|
|
const v = encodeURIComponent(window.__SHINE_BUILD_HASH__ || 'dev');
|
|
const cssFiles = ['./styles/main.css', './styles/layout.css', './styles/components.css', './styles/network-graph.css', './styles/buttons-white.css'];
|
|
cssFiles.forEach((file) => {
|
|
const link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.href = `${file}?v=${v}`;
|
|
document.head.appendChild(link);
|
|
});
|
|
}());
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="initial-splash" class="initial-splash" aria-label="Сияние">
|
|
<div class="initial-splash__logo-wrap">
|
|
<img
|
|
class="initial-splash__logo"
|
|
src="./img/shine-logo-transparent-final.png"
|
|
alt="Логотип Сияние"
|
|
/>
|
|
</div>
|
|
<div class="initial-splash__brand">Сияние</div>
|
|
</div>
|
|
<div class="app-shell">
|
|
<div id="topbar-slot" class="topbar-slot" hidden></div>
|
|
<main id="app-screen" class="screen-content"></main>
|
|
<div id="composer-slot" class="composer-slot" hidden></div>
|
|
<div id="toolbar-slot" class="toolbar-slot"></div>
|
|
</div>
|
|
<div id="modal-root"></div>
|
|
<script>
|
|
// Public VAPID key for Web Push (Base64URL)
|
|
window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__ = 'BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI';
|
|
</script>
|
|
<script>
|
|
(function attachBootErrorOverlay() {
|
|
const stateKey = '__SHINE_BOOT_ERROR_STATE__';
|
|
const menuId = 'boot-error-action-sheet';
|
|
const escapeText = (value) => String(value == null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
const safeString = (value, maxLen = 1000) => {
|
|
const text = String(value == null ? '' : value).trim();
|
|
if (text.length <= maxLen) return text;
|
|
return `${text.slice(0, Math.max(0, maxLen - 3))}...`;
|
|
};
|
|
const setState = (next) => {
|
|
try {
|
|
window[stateKey] = next;
|
|
} catch {}
|
|
};
|
|
const getState = () => {
|
|
try {
|
|
return window[stateKey] || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
const getKnownClientError = () => {
|
|
try {
|
|
return typeof window.__SHINE_GET_LAST_CLIENT_ERROR__ === 'function'
|
|
? window.__SHINE_GET_LAST_CLIENT_ERROR__()
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
const showFeedback = (message, kind = 'success') => {
|
|
try {
|
|
if (typeof window.__SHINE_SHOW_TOAST__ === 'function') {
|
|
window.__SHINE_SHOW_TOAST__(message, kind);
|
|
} else {
|
|
console.info(message);
|
|
}
|
|
} catch {}
|
|
};
|
|
const buildReport = () => {
|
|
const state = getState() || {};
|
|
const known = getKnownClientError();
|
|
const viewport = {
|
|
width: Math.round(window.innerWidth || 0),
|
|
height: Math.round(window.innerHeight || 0),
|
|
dpr: Number(window.devicePixelRatio || 1),
|
|
visualWidth: Math.round(window.visualViewport?.width || 0),
|
|
visualHeight: Math.round(window.visualViewport?.height || 0),
|
|
visualScale: Number(window.visualViewport?.scale || 1),
|
|
};
|
|
const screenInfo = window.screen ? {
|
|
width: Math.round(window.screen.width || 0),
|
|
height: Math.round(window.screen.height || 0),
|
|
availWidth: Math.round(window.screen.availWidth || 0),
|
|
availHeight: Math.round(window.screen.availHeight || 0),
|
|
pixelDepth: Number(window.screen.pixelDepth || 0),
|
|
} : null;
|
|
return {
|
|
kind: safeString(state.kind || 'boot_error', 64),
|
|
title: safeString(state.title || 'BOOT ERROR', 128),
|
|
message: safeString(state.message || '', 500),
|
|
stack: safeString(state.stack || '', 8000),
|
|
sourceUrl: safeString(state.sourceUrl || '', 240),
|
|
lineNumber: Number.isFinite(state.lineNumber) ? state.lineNumber : null,
|
|
columnNumber: Number.isFinite(state.columnNumber) ? state.columnNumber : null,
|
|
reasonType: safeString(state.reasonType || '', 64),
|
|
route: safeString(window.location.hash || window.location.pathname || '', 200),
|
|
href: safeString(window.location.href || '', 240),
|
|
pageTitle: safeString(document.title || '', 200),
|
|
pageVisibility: safeString(document.visibilityState || '', 32),
|
|
userAgent: safeString(navigator.userAgent || '', 240),
|
|
locale: safeString(navigator.language || '', 32),
|
|
clientTs: Number.isFinite(state.clientTs) ? state.clientTs : Date.now(),
|
|
viewport,
|
|
screenInfo,
|
|
lastKnownClientError: known || null,
|
|
contextJson: safeString(JSON.stringify({
|
|
bootState: state,
|
|
currentRoute: window.location.hash || window.location.pathname || '',
|
|
hasClientErrorSender: typeof window.__SHINE_SEND_CLIENT_ERROR__ === 'function',
|
|
}), 2000),
|
|
};
|
|
};
|
|
const buildSendPayload = (report) => {
|
|
const payload = {
|
|
kind: safeString(report?.kind || 'boot_error', 64),
|
|
message: safeString(report?.message || report?.title || 'Неизвестная ошибка', 500),
|
|
stack: safeString(report?.stack || '', 8000),
|
|
sourceUrl: safeString(report?.sourceUrl || '', 240),
|
|
lineNumber: Number.isFinite(report?.lineNumber) ? report.lineNumber : null,
|
|
columnNumber: Number.isFinite(report?.columnNumber) ? report.columnNumber : null,
|
|
route: safeString(report?.route || '', 200),
|
|
href: safeString(report?.href || '', 240),
|
|
userAgent: safeString(report?.userAgent || '', 240),
|
|
clientTs: Number.isFinite(report?.clientTs) ? report.clientTs : Date.now(),
|
|
requestOp: '',
|
|
requestIdRef: '',
|
|
contextJson: safeString(JSON.stringify({
|
|
...report,
|
|
contextJson: undefined,
|
|
}), 2000),
|
|
};
|
|
return payload;
|
|
};
|
|
const shareErrorText = async (report) => {
|
|
const text = formatReportText(report);
|
|
if (!text) throw new Error('Текст ошибки пуст');
|
|
if (!navigator.share) {
|
|
throw new Error('Отправка через системное меню недоступна в этом браузере');
|
|
}
|
|
try {
|
|
await navigator.share({
|
|
title: report?.title || 'Описание ошибки',
|
|
text,
|
|
});
|
|
} catch (error) {
|
|
if (error?.name === 'AbortError') return false;
|
|
throw error;
|
|
}
|
|
return true;
|
|
};
|
|
const formatReportText = (report) => {
|
|
const lines = [
|
|
`Ошибка: ${report.title || report.kind || 'unknown'}`,
|
|
`Описание: ${report.message || '—'}`,
|
|
`Окно: ${report.pageTitle || '—'}`,
|
|
`Маршрут: ${report.route || '—'}`,
|
|
`URL: ${report.href || '—'}`,
|
|
`Видимость: ${report.pageVisibility || '—'}`,
|
|
`Время: ${new Date(Number(report.clientTs || Date.now())).toISOString()}`,
|
|
`UA: ${report.userAgent || '—'}`,
|
|
`Экран: ${report.viewport ? `${report.viewport.width}x${report.viewport.height} @${report.viewport.dpr || 1}x` : '—'}`,
|
|
`Монитор: ${report.screenInfo ? `${report.screenInfo.width}x${report.screenInfo.height}` : '—'}`,
|
|
`Источник: ${report.sourceUrl || '—'}`,
|
|
`Строка: ${Number.isFinite(report.lineNumber) ? report.lineNumber : '—'}`,
|
|
`Колонка: ${Number.isFinite(report.columnNumber) ? report.columnNumber : '—'}`,
|
|
`Тип причины: ${report.reasonType || '—'}`,
|
|
];
|
|
if (report.stack) {
|
|
lines.push('Stack:', report.stack);
|
|
}
|
|
if (report.lastKnownClientError) {
|
|
lines.push('Последняя известная ошибка:', JSON.stringify(report.lastKnownClientError, null, 2));
|
|
}
|
|
if (report.contextJson) {
|
|
lines.push('Контекст:', report.contextJson);
|
|
}
|
|
return lines.join('\n');
|
|
};
|
|
const copyText = async (text) => {
|
|
const value = String(text || '');
|
|
if (!value) return false;
|
|
if (navigator.clipboard?.writeText) {
|
|
await navigator.clipboard.writeText(value);
|
|
return true;
|
|
}
|
|
const ta = document.createElement('textarea');
|
|
ta.value = value;
|
|
ta.setAttribute('readonly', 'readonly');
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
ta.style.pointerEvents = 'none';
|
|
document.body.append(ta);
|
|
ta.focus();
|
|
ta.select();
|
|
const ok = document.execCommand('copy');
|
|
ta.remove();
|
|
return !!ok;
|
|
};
|
|
const removeMenu = () => {
|
|
try {
|
|
document.getElementById(menuId)?.remove();
|
|
} catch {}
|
|
};
|
|
const openMenu = () => {
|
|
try {
|
|
removeMenu();
|
|
const report = buildReport();
|
|
const root = document.getElementById('modal-root') || document.body;
|
|
const shell = document.createElement('div');
|
|
shell.id = menuId;
|
|
shell.className = 'modal-shell boot-error-menu-shell';
|
|
shell.innerHTML = `
|
|
<div class="modal-backdrop" data-action="close"></div>
|
|
<div class="modal-dialog boot-error-menu-dialog" role="dialog" aria-modal="true" aria-labelledby="boot-error-menu-title" tabindex="-1">
|
|
<div class="modal-card stack boot-error-menu-card">
|
|
<strong class="modal-title" id="boot-error-menu-title">Описание ошибки</strong>
|
|
<p class="meta-muted boot-error-menu-message">${escapeText(report.message || report.title || 'Ошибка')}</p>
|
|
<button type="button" class="secondary-btn" data-action="copy">Скопировать текст ошибки</button>
|
|
<button type="button" class="secondary-btn" data-action="share">Отправить текст ошибки</button>
|
|
<button type="button" class="secondary-btn" data-action="send"${typeof window.__SHINE_SEND_CLIENT_ERROR_TO_SERVER__ === 'function' ? '' : ' disabled'}>Отправить в лог на сервере</button>
|
|
<button type="button" class="ghost-btn" data-action="close">Закрыть</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
root.append(shell);
|
|
|
|
const dialog = shell.querySelector('.modal-dialog');
|
|
const close = () => {
|
|
window.removeEventListener('keydown', onKeyDown);
|
|
shell.remove();
|
|
};
|
|
const onKeyDown = (event) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
close();
|
|
}
|
|
};
|
|
|
|
shell.addEventListener('click', (event) => {
|
|
if (event.target === shell || event.target?.dataset?.action === 'close') {
|
|
close();
|
|
}
|
|
});
|
|
|
|
shell.querySelector('[data-action="copy"]')?.addEventListener('click', async () => {
|
|
try {
|
|
await copyText(formatReportText(report));
|
|
showFeedback('Описание ошибки скопировано');
|
|
} catch {
|
|
showFeedback('Не удалось скопировать описание ошибки', 'error');
|
|
}
|
|
close();
|
|
});
|
|
|
|
shell.querySelector('[data-action="share"]')?.addEventListener('click', async () => {
|
|
try {
|
|
const shared = await shareErrorText(report);
|
|
if (shared === false) return close();
|
|
showFeedback('Текст ошибки открыт для отправки');
|
|
} catch (error) {
|
|
showFeedback(error?.message || 'Не удалось открыть системное меню отправки', 'error');
|
|
}
|
|
close();
|
|
});
|
|
|
|
shell.querySelector('[data-action="send"]')?.addEventListener('click', async () => {
|
|
try {
|
|
const sender = window.__SHINE_SEND_CLIENT_ERROR_TO_SERVER__;
|
|
if (typeof sender !== 'function') {
|
|
throw new Error('Отправка в лог на сервере недоступна');
|
|
}
|
|
const ok = await sender(buildSendPayload(report));
|
|
if (!ok) {
|
|
throw new Error('Не удалось отправить ошибку в лог на сервере');
|
|
}
|
|
showFeedback('Ошибка отправлена в лог на сервере');
|
|
} catch (error) {
|
|
showFeedback(error?.message || 'Не удалось отправить ошибку в лог на сервере', 'error');
|
|
}
|
|
close();
|
|
});
|
|
|
|
shell.querySelector('[data-action="close"]')?.addEventListener('click', close);
|
|
dialog?.focus?.();
|
|
window.addEventListener('keydown', onKeyDown);
|
|
} catch (error) {
|
|
console.warn('boot error menu failed', error);
|
|
}
|
|
};
|
|
const show = (title, text, extra = {}) => {
|
|
try {
|
|
const reportState = {
|
|
kind: safeString(extra.kind || title || 'boot_error', 64),
|
|
title: safeString(title || 'BOOT ERROR', 128),
|
|
message: safeString(extra.message || text || '', 500),
|
|
stack: safeString(extra.stack || '', 8000),
|
|
sourceUrl: safeString(extra.sourceUrl || extra.filename || '', 240),
|
|
lineNumber: Number.isFinite(extra.lineNumber) ? extra.lineNumber : (Number.isFinite(extra.lineno) ? extra.lineno : null),
|
|
columnNumber: Number.isFinite(extra.columnNumber) ? extra.columnNumber : (Number.isFinite(extra.colno) ? extra.colno : null),
|
|
reasonType: safeString(extra.reasonType || '', 64),
|
|
clientTs: Number.isFinite(extra.clientTs) ? extra.clientTs : Date.now(),
|
|
};
|
|
setState(reportState);
|
|
|
|
let el = document.getElementById('boot-error-overlay');
|
|
if (!el) {
|
|
el = document.createElement('pre');
|
|
el.id = 'boot-error-overlay';
|
|
el.style.position = 'fixed';
|
|
el.style.left = '8px';
|
|
el.style.right = '8px';
|
|
el.style.bottom = '8px';
|
|
el.style.maxHeight = '40vh';
|
|
el.style.overflow = 'auto';
|
|
el.style.padding = '10px';
|
|
el.style.margin = '0';
|
|
el.style.background = 'rgba(120, 0, 0, 0.92)';
|
|
el.style.color = '#fff';
|
|
el.style.fontSize = '12px';
|
|
el.style.lineHeight = '1.4';
|
|
el.style.zIndex = '999999';
|
|
el.style.whiteSpace = 'pre-wrap';
|
|
el.style.cursor = 'pointer';
|
|
el.style.userSelect = 'none';
|
|
el.style.webkitUserSelect = 'none';
|
|
el.style.touchAction = 'manipulation';
|
|
el.setAttribute('role', 'button');
|
|
el.setAttribute('tabindex', '0');
|
|
el.setAttribute('aria-haspopup', 'dialog');
|
|
document.body.appendChild(el);
|
|
el.addEventListener('click', openMenu);
|
|
el.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
openMenu();
|
|
}
|
|
});
|
|
}
|
|
el.textContent = `[BOOT ERROR] ${title}\n${String(text || '')}`;
|
|
el.setAttribute('aria-label', `${title}. Нажмите, чтобы открыть меню действий.`);
|
|
el.title = 'Нажмите, чтобы открыть меню действий';
|
|
} catch {}
|
|
};
|
|
window.addEventListener('error', (e) => {
|
|
show('window.error', `${e?.message || ''}\n${e?.filename || ''}:${e?.lineno || ''}:${e?.colno || ''}`, {
|
|
kind: 'window_error',
|
|
message: e?.message || '',
|
|
stack: e?.error?.stack || '',
|
|
sourceUrl: e?.filename || '',
|
|
lineNumber: e?.lineno,
|
|
columnNumber: e?.colno,
|
|
reasonType: e?.error?.constructor?.name || e?.constructor?.name || 'ErrorEvent',
|
|
});
|
|
});
|
|
window.addEventListener('unhandledrejection', (e) => {
|
|
const reason = e?.reason;
|
|
show('unhandledrejection', reason?.stack || reason?.message || String(reason || 'unknown'), {
|
|
kind: 'unhandled_rejection',
|
|
message: reason?.message || String(reason || 'Unhandled promise rejection'),
|
|
stack: reason?.stack || '',
|
|
reasonType: reason?.constructor?.name || typeof reason,
|
|
});
|
|
});
|
|
}());
|
|
</script>
|
|
<script>
|
|
(function attachAppWithBuildHash() {
|
|
const v = encodeURIComponent(window.__SHINE_BUILD_HASH__ || 'dev');
|
|
const script = document.createElement('script');
|
|
script.type = 'module';
|
|
script.src = `./js/app.js?v=${v}`;
|
|
document.body.appendChild(script);
|
|
}());
|
|
</script>
|
|
</body>
|
|
</html>
|