A chat no longer dies when the network moves under it. A NAT rebind, a lift, a Wi-Fi radio parking itself, a phone that dozed: the session repairs its own network path in place, and the messages typed meanwhile go out when it returns. Recovery is an ICE restart, which renegotiates only the transport path — the DTLS handshake, the session keys and the SCTP association carrying the data channel all sit above ICE and survive it. The renegotiation SDP therefore travels over the existing end-to-end encrypted, SAS-verified channel: no signalling service enters the design, and an attacker who cannot already decrypt the session cannot inject a reconnection. A restart is refused outright unless the DTLS fingerprint in the incoming SDP matches the live session's, so recovery can never re-point a conversation at a different peer. When the path is gone for good the session is ended and its data wiped rather than left half-alive: with no server there is nothing to re-signal through, and a conversation whose transport is gone should not leave its plaintext in an open tab. The two cases where that is already certain are recognised in seconds instead of being retried for two minutes — a channel that has delivered nothing at all since the drop cannot carry a renegotiation, and an ICE agent left bound to a network that no longer exists reports zero candidate pairs on every restart. Judging liveness was the hard part. Silence is not evidence of death: browsers freeze backgrounded tabs outright, and a frozen peer answers nothing while being perfectly healthy. What survives that freeze is ICE consent, which the browser runs in its network stack rather than on the page's thread — so a connected ICE state means a silent peer is asleep, and only a degraded one turns an unanswered probe into a teardown. The grace window before a restart is sized to the browser's own timings: 'disconnected' arrives after ~5s of missed consent responses and is held ~25s before 'failed', and that window exists for self-healing, so restarting at the start of it broke connections that were about to recover. Several long-standing bugs surfaced along the way and are fixed here: - handleHeartbeat() was dispatched to but never defined, so every inbound heartbeat threw a TypeError and peer liveness was never observed at all. - Heartbeats were folded into the 5-minute maintenance cycle instead of running on their own timer, far too coarse to notice a dead path. - ondatachannel can hand over a channel that is already open, so the answering side's 'open' event had been dispatched before the handler was assigned and never fired, leaving that side with no heartbeat, no watchdog and no file-transfer init. The peer whose network was fine kept showing "connected" indefinitely because nothing was running to notice. - Answering a heartbeat required the peer to have finished verifying, but the two sides confirm a SAS code at different moments; for that whole window one of them could not reply and was declared dead on a healthy connection. - Sending on a channel that was not ready returned in silence: the text stayed in the box, nothing was transmitted, and nothing said why. - The send path gated on navigator.onLine and the offline/online events, which report whether an interface exists rather than whether anything is reachable. A tab the OS froze misses the 'online' edge, and this side then queued every message forever: one tick on everything it sent, while incoming messages kept arriving. Sending is now decided by the data channel, and queues drain by polling rather than on an edge, so a missed event cannot strand them. - A false offline modal appeared on a working session, because the offline event was taken at face value. tests/session-recovery.test.mjs covers the state machine, the backoff and its serialisation, the offline hold, the sleeping-peer discriminator and the identity check.
230 lines
21 KiB
HTML
230 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
|
<meta http-equiv="Content-Security-Policy"
|
|
content="default-src 'self';
|
|
script-src 'self';
|
|
style-src 'self' 'unsafe-inline';
|
|
font-src 'self' data:;
|
|
connect-src 'self' stun: stuns: turn: turns:;
|
|
img-src 'self' data:;
|
|
media-src 'self' blob:;
|
|
object-src 'none';
|
|
frame-src 'none';
|
|
worker-src 'self';
|
|
manifest-src 'self';
|
|
form-action 'self';
|
|
base-uri 'none';
|
|
upgrade-insecure-requests;">
|
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
|
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
|
|
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
|
|
|
<!-- PWA Manifest -->
|
|
<link rel="manifest" href="./manifest.json">
|
|
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785719685129">
|
|
|
|
<!-- PWA Meta Tags -->
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="format-detection" content="telephone=no">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="SecureBit">
|
|
<meta name="application-name" content="SecureBit">
|
|
<meta name="msapplication-TileColor" content="#ff6b35">
|
|
<meta name="msapplication-config" content="/browserconfig.xml">
|
|
|
|
<!-- iOS Splash Screens - Полная конфигурация с генератора -->
|
|
<!-- Landscape -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_17_Pro_Max__iPhone_16_Pro_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_17_Pro__iPhone_17__iPhone_16_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 420px) and (device-height: 912px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_Air_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_16e__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_11__iPhone_XR_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png">
|
|
|
|
<!-- iPad Landscape -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/13__iPad_Pro_M4_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/12.9__iPad_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/11__iPad_Pro_M4_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/10.9__iPad_Air_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/10.5__iPad_Air_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/10.2__iPad_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/8.3__iPad_Mini_landscape.png">
|
|
|
|
<!-- Portrait -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_17_Pro_Max__iPhone_16_Pro_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_17_Pro__iPhone_17__iPhone_16_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 420px) and (device-height: 912px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_Air_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_16e__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_11__iPhone_XR_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png">
|
|
|
|
<!-- iPad Portrait -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/13__iPad_Pro_M4_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/12.9__iPad_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/11__iPad_Pro_M4_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/10.9__iPad_Air_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/10.5__iPad_Air_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/10.2__iPad_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
|
|
|
<!-- Apple Touch Icons -->
|
|
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785719685129">
|
|
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
|
|
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
|
|
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
|
|
<link rel="apple-touch-icon" sizes="76x76" href="./logo/icon-76x76.png">
|
|
<link rel="apple-touch-icon" sizes="114x114" href="./logo/icon-114x114.png">
|
|
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
|
|
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
|
|
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785719685129">
|
|
|
|
<!-- Microsoft Tiles -->
|
|
<meta name="msapplication-TileColor" content="#ff6b35">
|
|
<meta name="msapplication-TileImage" content="./logo/icon-144x144.png">
|
|
<meta name="msapplication-config" content="./browserconfig.xml">
|
|
|
|
<!-- Theme colors -->
|
|
<meta name="theme-color" content="#ff6b35">
|
|
<meta name="msapplication-navbutton-color" content="#ff6b35">
|
|
|
|
<!-- Security Headers for PWA - CSP is already defined above -->
|
|
|
|
|
|
<!-- SEO -->
|
|
<title>SecureBit.chat — Private, Encrypted Messenger</title>
|
|
<meta name="description" content="SecureBit.chat is a free, end-to-end encrypted peer-to-peer messenger that runs entirely in your browser. No accounts, no servers storing your messages — ECDH P-384, AES-256-GCM and in-person SAS verification.">
|
|
<meta name="keywords" content="encrypted messenger, end-to-end encryption, peer-to-peer chat, P2P messenger, ECDH, DTLS, SAS verification, WebRTC, private chat, secure chat, no servers, no accounts, MITM protection, PFS">
|
|
<meta name="author" content="Volodymyr">
|
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
|
<link rel="canonical" href="https://securebit.chat/">
|
|
|
|
<!-- Open Graph -->
|
|
<meta property="og:site_name" content="SecureBit.chat">
|
|
<meta property="og:title" content="SecureBit.chat — End-to-End Encrypted Peer-to-Peer Chat">
|
|
<meta property="og:description" content="A secure messenger that runs entirely in your browser. No accounts, no servers storing your messages. Bring your own STUN/TURN servers — we don't impose anything.">
|
|
<meta property="og:url" content="https://securebit.chat/">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:locale" content="en_US">
|
|
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
|
<meta property="og:image:secure_url" content="https://securebit.chat/assets/social-card.png">
|
|
<meta property="og:image:type" content="image/png">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:image:alt" content="SecureBit.chat — end-to-end encrypted, peer-to-peer chat">
|
|
|
|
<!-- Twitter Card -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="SecureBit.chat — End-to-End Encrypted Peer-to-Peer Chat">
|
|
<meta name="twitter:description" content="A secure messenger that runs entirely in your browser. No accounts, no servers storing your messages.">
|
|
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
|
<meta name="twitter:image:alt" content="SecureBit.chat — end-to-end encrypted, peer-to-peer chat">
|
|
|
|
<!-- Structured data (schema.org) -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@graph": [
|
|
{
|
|
"@type": "WebSite",
|
|
"@id": "https://securebit.chat/#website",
|
|
"name": "SecureBit.chat",
|
|
"url": "https://securebit.chat/",
|
|
"description": "End-to-end encrypted, peer-to-peer chat that runs entirely in your browser.",
|
|
"inLanguage": "en"
|
|
},
|
|
{
|
|
"@type": "WebApplication",
|
|
"name": "SecureBit.chat",
|
|
"url": "https://securebit.chat/",
|
|
"applicationCategory": "CommunicationApplication",
|
|
"operatingSystem": "Any (modern web browser)",
|
|
"browserRequirements": "Requires WebRTC and the Web Crypto API",
|
|
"description": "A free, end-to-end encrypted peer-to-peer messenger. No accounts, no servers storing messages — ECDH P-384, AES-256-GCM and in-person SAS verification.",
|
|
"image": "https://securebit.chat/assets/social-card.png",
|
|
"license": "https://opensource.org/licenses/MIT",
|
|
"isAccessibleForFree": true,
|
|
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
|
|
"featureList": [
|
|
"End-to-end encryption (ECDH P-384, AES-256-GCM)",
|
|
"Peer-to-peer WebRTC connection with no message servers",
|
|
"In-person SAS verification against man-in-the-middle attacks",
|
|
"Custom STUN/TURN servers and relay-only privacy mode",
|
|
"Encrypted file transfer",
|
|
"Installable Progressive Web App with offline support"
|
|
],
|
|
"sameAs": [ "https://github.com/SecureBitChat/securebit-chat" ]
|
|
}
|
|
]
|
|
}
|
|
</script>
|
|
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
|
both execute in document order after parsing, so React still runs before the
|
|
app modules below, but the parser / first paint is no longer blocked. -->
|
|
<script defer src="config/ice-servers.js?v=1785719685129"></script>
|
|
<script defer src="libs/react/react.production.min.js"></script>
|
|
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
|
|
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
|
|
it never executes the snippet. Loaded in manual mode (no auto-highlight).
|
|
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
|
|
<script defer src="libs/prism/prism.js"></script>
|
|
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
|
|
<link rel="stylesheet" href="assets/tailwind.css?v=1785719685129">
|
|
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785719685129">
|
|
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
|
|
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
|
|
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
|
|
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
|
|
<link rel="stylesheet" href="src/styles/main.css?v=1785719685129">
|
|
<link rel="stylesheet" href="src/styles/animations.css?v=1785719685129">
|
|
<link rel="stylesheet" href="src/styles/components.css?v=1785719685129">
|
|
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
|
<script defer src="src/scripts/load-async-css.js?v=1785719685129"></script>
|
|
<noscript>
|
|
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
|
<link rel="stylesheet" href="libs/prism/prism.css">
|
|
</noscript>
|
|
<script defer src="src/scripts/fa-check.js?v=1785719685129"></script>
|
|
<!-- Update Manager - система принудительного обновления -->
|
|
<script defer src="src/utils/updateManager.js?v=1785719685129"></script>
|
|
<script type="module" src="src/components/UpdateChecker.jsx?v=1785719685129"></script>
|
|
<script type="module" src="dist/qr-local.js?v=1785719685129"></script>
|
|
<script type="module" src="src/components/QRScanner.js?v=1785719685129"></script>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="module" src="dist/app-boot.js?v=1785719685129"></script>
|
|
<script type="module" src="dist/app.js?v=1785719685129"></script>
|
|
|
|
<script defer src="src/scripts/pwa-register.js?v=1785719685129"></script>
|
|
<script src="./src/pwa/install-prompt.js?v=1785719685129" type="module"></script>
|
|
<script src="./src/pwa/pwa-manager.js?v=1785719685129" type="module"></script>
|
|
<script defer src="./src/scripts/pwa-offline-test.js?v=1785719685129"></script>
|
|
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785719685129">
|
|
</body>
|
|
</html> |