Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints. Co-authored-by: Cursor <cursoragent@cursor.com>
834 lines
25 KiB
HTML
834 lines
25 KiB
HTML
<!doctype html>
|
|
<html lang="fa" dir="rtl">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<script>
|
|
(function () {
|
|
try {
|
|
var icon = ''
|
|
try {
|
|
icon = localStorage.getItem('PWA_INSTALL_ICON_URL') || ''
|
|
} catch (e) {}
|
|
var title = ''
|
|
try {
|
|
var raw = localStorage.getItem('PWA_MANIFEST_CACHE_V5')
|
|
if (raw) {
|
|
var payload = JSON.parse(raw)
|
|
var manifest = payload && payload.manifest
|
|
if (manifest) {
|
|
title = manifest.short_name || manifest.name || ''
|
|
if (!icon && manifest.icons && manifest.icons.length) {
|
|
var icons = manifest.icons
|
|
var pick = function (size) {
|
|
var target = size + 'x' + size
|
|
for (var i = 0; i < icons.length; i++) {
|
|
if (String(icons[i].sizes || '').indexOf(target) !== -1 && icons[i].src) {
|
|
return icons[i].src
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
icon = pick(180) || pick(192) || pick(512) || (icons[0] && icons[0].src) || ''
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
if (title) {
|
|
document.title = title
|
|
var appleTitle = document.createElement('meta')
|
|
appleTitle.name = 'apple-mobile-web-app-title'
|
|
appleTitle.content = title
|
|
document.head.appendChild(appleTitle)
|
|
}
|
|
if (icon) {
|
|
var sized = document.createElement('link')
|
|
sized.rel = 'apple-touch-icon'
|
|
sized.setAttribute('data-dynamic-pwa', 'true')
|
|
sized.setAttribute('data-pwa-icon-src', icon)
|
|
sized.setAttribute('sizes', '180x180')
|
|
sized.href = icon
|
|
document.head.appendChild(sized)
|
|
var primary = document.createElement('link')
|
|
primary.rel = 'apple-touch-icon'
|
|
primary.setAttribute('data-dynamic-pwa', 'true')
|
|
primary.setAttribute('data-pwa-icon-src', icon)
|
|
primary.href = icon
|
|
document.head.appendChild(primary)
|
|
var img = new Image()
|
|
img.decoding = 'async'
|
|
img.src = icon
|
|
}
|
|
} catch (e) {}
|
|
})()
|
|
</script>
|
|
<meta name="enamad" content="44889195" />
|
|
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
<meta http-equiv="Pragma" content="no-cache" />
|
|
<meta http-equiv="Expires" content="0" />
|
|
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
|
/>
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
|
|
<title>در حال بارگذاری...</title>
|
|
<!---->
|
|
<style>
|
|
:root {
|
|
--boot-bg: #133050;
|
|
--boot-bg-soft: #133050;
|
|
--boot-card: rgba(255, 255, 255, 0.88);
|
|
--boot-border: rgba(15, 23, 42, 0.08);
|
|
--boot-text: #111827;
|
|
--boot-muted: rgba(15, 23, 42, 0.62);
|
|
--boot-accent: #2563eb;
|
|
--boot-orb: rgba(37, 99, 235, 0.10);
|
|
--boot-spinner-track: rgba(15, 23, 42, 0.10);
|
|
--boot-shadow: rgba(15, 23, 42, 0.10);
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
min-height: 100%;
|
|
width: 100%;
|
|
background: var(--boot-bg);
|
|
}
|
|
|
|
#app {
|
|
min-height: 100vh;
|
|
width: 100%;
|
|
max-width: none;
|
|
}
|
|
|
|
.boot-loader {
|
|
min-height: 100vh;
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
direction: rtl;
|
|
font-family:
|
|
system-ui,
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
sans-serif;
|
|
background: var(--boot-bg);
|
|
color: var(--boot-text);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.boot-loader__logo {
|
|
width: min(72vw, 280px);
|
|
height: auto;
|
|
object-fit: contain;
|
|
animation: boot-logo-fade-in 0.45s ease-out both;
|
|
}
|
|
|
|
@keyframes boot-logo-fade-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.96);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
/* Critical: full-viewport splash before Vue/CSS hydrate */
|
|
@media (min-width: 768px) {
|
|
html, body, #app {
|
|
width: 100%;
|
|
max-width: none;
|
|
}
|
|
body {
|
|
display: block !important;
|
|
}
|
|
}
|
|
#app,
|
|
.boot-loader {
|
|
width: 100% !important;
|
|
max-width: none !important;
|
|
}
|
|
.app-splash-screen {
|
|
position: fixed !important;
|
|
inset: 0 !important;
|
|
width: 100vw !important;
|
|
min-width: 100vw !important;
|
|
max-width: none !important;
|
|
min-height: 100vh !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
margin: 0 !important;
|
|
transform: none !important;
|
|
z-index: 2147483646;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
if (window.matchMedia('(min-width: 768px)').matches) {
|
|
document.documentElement.classList.add('app-layout--desktop')
|
|
}
|
|
})()
|
|
</script>
|
|
|
|
<script>
|
|
;(function () {
|
|
var CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE'
|
|
|
|
function getCookie(name) {
|
|
var cookie = document.cookie.split('; ').find(function (row) {
|
|
return row.indexOf(name + '=') === 0
|
|
})
|
|
|
|
if (!cookie) return null
|
|
|
|
return cookie.substring(name.length + 1)
|
|
}
|
|
|
|
function readCachedConfig() {
|
|
try {
|
|
var rawCookie = getCookie(CONFIG_COOKIE_NAME)
|
|
|
|
if (!rawCookie) return null
|
|
|
|
var decoded = decodeURIComponent(rawCookie)
|
|
var payload = JSON.parse(decoded)
|
|
|
|
return payload && payload.config ? payload.config : null
|
|
} catch (error) {
|
|
console.warn('Boot theme: failed to read cached config', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function normalizeHex(hex) {
|
|
if (!hex || typeof hex !== 'string') return null
|
|
|
|
var value = hex.trim()
|
|
|
|
if (!value) return null
|
|
|
|
if (value[0] !== '#') {
|
|
value = '#' + value
|
|
}
|
|
|
|
if (/^#[0-9a-fA-F]{3}$/.test(value)) {
|
|
value =
|
|
'#' +
|
|
value[1] +
|
|
value[1] +
|
|
value[2] +
|
|
value[2] +
|
|
value[3] +
|
|
value[3]
|
|
}
|
|
|
|
if (!/^#[0-9a-fA-F]{6}$/.test(value)) return null
|
|
|
|
return value
|
|
}
|
|
|
|
function hexToRgb(hex) {
|
|
var normalized = normalizeHex(hex)
|
|
|
|
if (!normalized) return null
|
|
|
|
var num = parseInt(normalized.slice(1), 16)
|
|
|
|
return {
|
|
r: (num >> 16) & 255,
|
|
g: (num >> 8) & 255,
|
|
b: num & 255
|
|
}
|
|
}
|
|
|
|
function rgbToHex(r, g, b) {
|
|
function toHex(value) {
|
|
var hex = Math.max(0, Math.min(255, Math.round(value))).toString(16)
|
|
return hex.length === 1 ? '0' + hex : hex
|
|
}
|
|
|
|
return '#' + toHex(r) + toHex(g) + toHex(b)
|
|
}
|
|
|
|
function mix(hexA, hexB, weight) {
|
|
var a = hexToRgb(hexA)
|
|
var b = hexToRgb(hexB)
|
|
|
|
if (!a || !b) return hexA
|
|
|
|
return rgbToHex(
|
|
a.r * (1 - weight) + b.r * weight,
|
|
a.g * (1 - weight) + b.g * weight,
|
|
a.b * (1 - weight) + b.b * weight
|
|
)
|
|
}
|
|
|
|
function brightness(hex) {
|
|
var rgb = hexToRgb(hex)
|
|
|
|
if (!rgb) return 0
|
|
|
|
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
|
|
}
|
|
|
|
function hexToRgba(hex, alpha) {
|
|
var rgb = hexToRgb(hex)
|
|
|
|
if (!rgb) return 'rgba(96, 165, 250, ' + alpha + ')'
|
|
|
|
return 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + alpha + ')'
|
|
}
|
|
|
|
function setVar(name, value) {
|
|
document.documentElement.style.setProperty(name, value)
|
|
}
|
|
|
|
function applyBootTheme() {
|
|
var config = readCachedConfig()
|
|
var user = config && config.user ? config.user : null
|
|
|
|
if (!user) return
|
|
|
|
var tenantColor = normalizeHex(user.color)
|
|
var tenantSecondary = normalizeHex(user.secondary_color)
|
|
var tenantBackground = normalizeHex(user.background_color)
|
|
|
|
/**
|
|
* مهم:
|
|
* background_color بکگراند اپ است.
|
|
* اگر نبود، لودینگ باید روشن بماند، نه اینکه color را بکگراند کنیم.
|
|
*/
|
|
var bg = tenantBackground || '#ffffff'
|
|
|
|
/**
|
|
* color و secondary_color فقط برای accent استفاده میشوند.
|
|
*/
|
|
var accent = tenantSecondary || tenantColor || '#2563eb'
|
|
|
|
var isLight = brightness(bg) > 155
|
|
|
|
var bgSoft = isLight ? mix(bg, accent, 0.045) : mix(bg, '#ffffff', 0.055)
|
|
|
|
var text = isLight ? '#111827' : '#ffffff'
|
|
var muted = isLight ? 'rgba(15, 23, 42, 0.62)' : 'rgba(255, 255, 255, 0.70)'
|
|
|
|
var card = isLight ? 'rgba(255, 255, 255, 0.88)' : 'rgba(255, 255, 255, 0.07)'
|
|
var border = isLight ? 'rgba(15, 23, 42, 0.08)' : 'rgba(255, 255, 255, 0.12)'
|
|
|
|
var orb = hexToRgba(accent, isLight ? 0.10 : 0.18)
|
|
var spinnerTrack = isLight ? 'rgba(15, 23, 42, 0.10)' : 'rgba(255, 255, 255, 0.14)'
|
|
var shadow = isLight ? 'rgba(15, 23, 42, 0.10)' : 'rgba(0, 0, 0, 0.24)'
|
|
|
|
setVar('--boot-bg', bg)
|
|
setVar('--boot-bg-soft', bgSoft)
|
|
setVar('--boot-accent', accent)
|
|
setVar('--boot-text', text)
|
|
setVar('--boot-muted', muted)
|
|
setVar('--boot-card', card)
|
|
setVar('--boot-border', border)
|
|
setVar('--boot-orb', orb)
|
|
setVar('--boot-spinner-track', spinnerTrack)
|
|
setVar('--boot-shadow', shadow)
|
|
|
|
if (tenantColor) {
|
|
setVar('--App-primary-color', tenantColor)
|
|
}
|
|
if (tenantSecondary) {
|
|
setVar('--App-secondary-color', tenantSecondary)
|
|
}
|
|
if (tenantBackground) {
|
|
setVar('--App-background-color', tenantBackground)
|
|
}
|
|
|
|
var logoEl = document.getElementById('boot-logo')
|
|
if (logoEl) {
|
|
if (user.name === 'boursecu') {
|
|
logoEl.src = '/boursecu-splash.png'
|
|
} else if (user.avatar) {
|
|
logoEl.src = user.avatar
|
|
}
|
|
}
|
|
|
|
if (user.name === 'boursecu') {
|
|
setVar('--boot-bg', '#133050')
|
|
setVar('--boot-bg-soft', '#133050')
|
|
}
|
|
|
|
document.title = user.name_fa || user.name || 'در حال بارگذاری...'
|
|
}
|
|
|
|
applyBootTheme()
|
|
})()
|
|
</script>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="app">
|
|
<div class="boot-loader">
|
|
<img
|
|
id="boot-logo"
|
|
class="boot-loader__logo"
|
|
src="/boursecu-splash.png"
|
|
alt=""
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
;(function () {
|
|
var BOOT_TIMEOUT_MS = 90000
|
|
|
|
var RETRY_KEY = '__APP_BOOT_RETRY_CLICKED__'
|
|
var RECOVERY_KEY = '__APP_BOOT_RECOVERY_DONE__'
|
|
|
|
var CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE'
|
|
var CONFIG_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7
|
|
|
|
var TOKEN_STORAGE_KEY = 'customer_session_token'
|
|
var LEGACY_TOKEN_SUFFIX = '_app_token'
|
|
var VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
|
|
var VERSION_STORAGE_KEY = 'version'
|
|
var CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
|
|
|
function getProtocol() {
|
|
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
|
}
|
|
|
|
function resolveTenantDomain() {
|
|
var host = String(window.location.hostname || '').trim()
|
|
if (!host || host === 'localhost' || host === '127.0.0.1') {
|
|
return 'k.montzar.ir'
|
|
}
|
|
return host
|
|
}
|
|
|
|
function getSuperAdminBases() {
|
|
return [
|
|
'https://adm.montzar.ir',
|
|
'https://adm.montech.ir',
|
|
'https://adm.montest.ir'
|
|
]
|
|
}
|
|
|
|
function getVersionUrls() {
|
|
var domain = encodeURIComponent(resolveTenantDomain())
|
|
return getSuperAdminBases().map(function (base) {
|
|
return base + '/get_version?domain=' + domain
|
|
})
|
|
}
|
|
|
|
function getConfigUrls() {
|
|
var domain = encodeURIComponent(resolveTenantDomain())
|
|
return getSuperAdminBases().map(function (base) {
|
|
return base + '/get_user?domain=' + domain
|
|
})
|
|
}
|
|
|
|
// پاککردن URL موفقیت قدیمی رجیستری (مثل nerkh.gold)
|
|
;(function clearLegacyConfigSuccess() {
|
|
var last = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
|
if (!last) return
|
|
var ok = getSuperAdminBases().some(function (base) {
|
|
return last.indexOf(base) === 0
|
|
})
|
|
if (!ok) {
|
|
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
|
document.cookie = CONFIG_COOKIE_NAME + '=; Path=/; Max-Age=0; SameSite=Lax'
|
|
}
|
|
})()
|
|
|
|
function sortVersionUrls(urls) {
|
|
var lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
|
|
|
|
if (!lastSuccessUrl) {
|
|
return urls.slice()
|
|
}
|
|
|
|
return [lastSuccessUrl].concat(urls.filter(function (url) {
|
|
return url !== lastSuccessUrl
|
|
}))
|
|
}
|
|
|
|
function sortByLastSuccess(urls) {
|
|
var lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
|
|
|
if (lastSuccessUrl && urls.indexOf(lastSuccessUrl) === -1) {
|
|
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
|
lastSuccessUrl = null
|
|
}
|
|
|
|
return urls.slice().sort(function (a, b) {
|
|
if (a === lastSuccessUrl) return -1
|
|
if (b === lastSuccessUrl) return 1
|
|
return 0
|
|
})
|
|
}
|
|
|
|
function normalizeVersionResponse(data) {
|
|
if (typeof data === 'string' || typeof data === 'number') {
|
|
return String(data).trim()
|
|
}
|
|
|
|
if (!data) return ''
|
|
|
|
if (typeof data.data === 'string' || typeof data.data === 'number') {
|
|
return String(data.data).trim()
|
|
}
|
|
|
|
var version =
|
|
data.version ||
|
|
data.value ||
|
|
(data.data && typeof data.data === 'object' ? data.data.version : '')
|
|
|
|
return version ? String(version).trim() : ''
|
|
}
|
|
|
|
async function fetchJsonOrText(url, timeout) {
|
|
var controller = new AbortController()
|
|
|
|
var timeoutId = window.setTimeout(function () {
|
|
controller.abort()
|
|
}, timeout)
|
|
|
|
try {
|
|
var response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json'
|
|
},
|
|
cache: 'no-store',
|
|
signal: controller.signal
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Request failed: ' + response.status)
|
|
}
|
|
|
|
var contentType = response.headers.get('content-type') || ''
|
|
|
|
if (contentType.indexOf('application/json') !== -1) {
|
|
return await response.json()
|
|
}
|
|
|
|
return await response.text()
|
|
} finally {
|
|
window.clearTimeout(timeoutId)
|
|
}
|
|
}
|
|
|
|
async function fetchVersionFromUrl(url, timeout) {
|
|
var controller = new AbortController()
|
|
var timeoutId = window.setTimeout(function () {
|
|
controller.abort()
|
|
}, timeout)
|
|
|
|
try {
|
|
var response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json'
|
|
},
|
|
cache: 'no-store',
|
|
signal: controller.signal
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Version request failed: ' + response.status)
|
|
}
|
|
|
|
var contentType = response.headers.get('content-type') || ''
|
|
var data
|
|
|
|
if (contentType.indexOf('application/json') !== -1) {
|
|
data = await response.json()
|
|
} else {
|
|
data = await response.text()
|
|
}
|
|
|
|
var version = normalizeVersionResponse(data)
|
|
|
|
if (!version) {
|
|
throw new Error('Version response is empty')
|
|
}
|
|
|
|
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
|
|
return version
|
|
} catch (error) {
|
|
if (localStorage.getItem(VERSION_SUCCESS_URL_KEY) === url) {
|
|
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
|
}
|
|
console.warn('Version failed:', url, error)
|
|
return undefined
|
|
} finally {
|
|
window.clearTimeout(timeoutId)
|
|
}
|
|
}
|
|
|
|
async function loadRemoteVersion(rounds) {
|
|
var urls = sortVersionUrls(getVersionUrls())
|
|
|
|
for (var round = 1; round <= rounds; round++) {
|
|
for (var i = 0; i < urls.length; i++) {
|
|
var timeout = i === urls.length - 1 ? 6000 : 3500
|
|
var result = await fetchVersionFromUrl(urls[i], timeout)
|
|
|
|
if (result) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
async function fetchFreshConfig(rounds) {
|
|
var urls = sortByLastSuccess(getConfigUrls())
|
|
|
|
for (var round = 1; round <= rounds; round++) {
|
|
for (var i = 0; i < urls.length; i++) {
|
|
var url = urls[i]
|
|
|
|
try {
|
|
var config = await fetchJsonOrText(url, i === urls.length - 1 ? 7000 : 4000)
|
|
|
|
if (!config || typeof config !== 'object') {
|
|
throw new Error('Config response is invalid')
|
|
}
|
|
|
|
return {
|
|
config: config,
|
|
url: url
|
|
}
|
|
} catch (error) {
|
|
if (localStorage.getItem(CONFIG_SUCCESS_URL_KEY) === url) {
|
|
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
|
}
|
|
console.warn('Config failed:', url, 'round:', round, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error('Cannot fetch fresh config')
|
|
}
|
|
|
|
function writeConfigCookie(config) {
|
|
try {
|
|
var payload = {
|
|
cached_at: Date.now(),
|
|
config: config
|
|
}
|
|
|
|
var encoded = encodeURIComponent(JSON.stringify(payload))
|
|
var secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
|
|
|
document.cookie =
|
|
CONFIG_COOKIE_NAME +
|
|
'=' +
|
|
encoded +
|
|
'; Path=/; Max-Age=' +
|
|
CONFIG_COOKIE_MAX_AGE_SECONDS +
|
|
'; SameSite=Lax' +
|
|
secure
|
|
} catch (error) {
|
|
console.warn('Failed to write config cookie:', error)
|
|
}
|
|
}
|
|
|
|
function clearReadableCookies() {
|
|
document.cookie.split(';').forEach(function (cookie) {
|
|
var name = cookie.split('=')[0] && cookie.split('=')[0].trim()
|
|
|
|
if (!name) return
|
|
|
|
document.cookie = name + '=; Path=/; Max-Age=0; SameSite=Lax'
|
|
})
|
|
}
|
|
|
|
function collectAuthTokensForPreserve() {
|
|
var preserved = {}
|
|
|
|
var session = localStorage.getItem(TOKEN_STORAGE_KEY)
|
|
if (session) {
|
|
preserved[TOKEN_STORAGE_KEY] = session
|
|
}
|
|
|
|
for (var i = 0; i < localStorage.length; i++) {
|
|
var key = localStorage.key(i)
|
|
if (!key) continue
|
|
if (key.length >= LEGACY_TOKEN_SUFFIX.length &&
|
|
key.slice(-LEGACY_TOKEN_SUFFIX.length) === LEGACY_TOKEN_SUFFIX) {
|
|
var value = localStorage.getItem(key)
|
|
if (value) {
|
|
preserved[key] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
return preserved
|
|
}
|
|
|
|
function clearLocalStorageExceptToken() {
|
|
var preserved = collectAuthTokensForPreserve()
|
|
|
|
localStorage.clear()
|
|
|
|
Object.keys(preserved).forEach(function (key) {
|
|
localStorage.setItem(key, preserved[key])
|
|
})
|
|
}
|
|
|
|
async function clearServiceWorkersAndCaches() {
|
|
try {
|
|
if ('serviceWorker' in navigator) {
|
|
var registrations = await navigator.serviceWorker.getRegistrations()
|
|
|
|
await Promise.all(
|
|
registrations.map(function (registration) {
|
|
return registration.unregister()
|
|
})
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to unregister service workers:', error)
|
|
}
|
|
|
|
try {
|
|
if ('caches' in window) {
|
|
var keys = await caches.keys()
|
|
|
|
await Promise.all(
|
|
keys.map(function (key) {
|
|
return caches.delete(key)
|
|
})
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to clear caches:', error)
|
|
}
|
|
}
|
|
|
|
function reloadWithCacheBust() {
|
|
var url = new URL(window.location.href)
|
|
url.searchParams.set('__boot_recover', String(Date.now()))
|
|
window.location.replace(url.toString())
|
|
}
|
|
|
|
function renderRecovering(text) {
|
|
var app = document.getElementById('app')
|
|
if (!app) return
|
|
|
|
app.innerHTML =
|
|
'<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; direction: rtl; font-family: sans-serif; padding: 24px; background: var(--boot-bg); color: var(--boot-text);">' +
|
|
'<div style="width: min(420px, 100%); text-align: center; background: var(--boot-card); border: 1px solid var(--boot-border); border-radius: 24px; padding: 28px 22px;">' +
|
|
'<div style="width: 54px; height: 54px; margin: 0 auto 18px; border-radius: 999px; border: 4px solid rgba(255,255,255,0.18); border-top-color: var(--boot-accent); animation: bootSpin 0.9s linear infinite;"></div>' +
|
|
'<style>@keyframes bootSpin { to { transform: rotate(360deg); } }</style>' +
|
|
'<h2 style="margin: 0 0 12px; font-size: 20px;">در حال بازیابی برنامه</h2>' +
|
|
'<p style="margin: 0; color: var(--boot-muted); line-height: 2; font-size: 14px;">' +
|
|
(text || 'لطفاً چند لحظه صبر کنید.') +
|
|
'</p>' +
|
|
'</div>' +
|
|
'</div>'
|
|
}
|
|
|
|
function renderBootError() {
|
|
var app = document.getElementById('app')
|
|
if (!app) return
|
|
|
|
app.innerHTML =
|
|
'<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; direction: rtl; font-family: sans-serif; padding: 24px; background: var(--boot-bg); color: var(--boot-text);">' +
|
|
'<div style="width: min(420px, 100%); text-align: center; background: var(--boot-card); border: 1px solid var(--boot-border); border-radius: 24px; padding: 28px 22px;">' +
|
|
'<div style="font-size: 42px; margin-bottom: 12px;">⚠️</div>' +
|
|
'<h2 style="margin: 0 0 12px; font-size: 20px;">خطا در بارگذاری برنامه</h2>' +
|
|
'<p style="margin: 0 0 20px; color: var(--boot-muted); line-height: 2; font-size: 14px;">' +
|
|
'برقراری ارتباط با سرور امکانپذیر نیست. لطفاً اتصال اینترنت خود را بررسی کرده و صفحه را تازهسازی نمایید.' +
|
|
'</p>' +
|
|
'<button onclick="window.__APP_BOOT_SIMPLE_RETRY__()" style="padding: 11px 22px; border: 0; border-radius: 12px; background: var(--boot-accent); color: white; cursor: pointer; font-weight: 700;">' +
|
|
'تازهسازی صفحه' +
|
|
'</button>' +
|
|
'<button onclick="window.__APP_BOOT_CLEAN_RELOAD__()" style="margin-right: 8px; padding: 11px 18px; border: 1px solid var(--boot-border); border-radius: 12px; background: transparent; color: var(--boot-text); cursor: pointer; font-weight: 700;">' +
|
|
'پاکسازی و تلاش مجدد' +
|
|
'</button>' +
|
|
'</div>' +
|
|
'</div>'
|
|
}
|
|
|
|
async function cleanFetchAndReload() {
|
|
renderRecovering('در حال دریافت تنظیمات جدید برنامه...')
|
|
|
|
var version = await loadRemoteVersion(3)
|
|
var freshConfigResult = await fetchFreshConfig(3)
|
|
|
|
clearReadableCookies()
|
|
// clearLocalStorageExceptToken()
|
|
await clearServiceWorkersAndCaches()
|
|
|
|
writeConfigCookie(freshConfigResult.config)
|
|
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, freshConfigResult.url)
|
|
|
|
if (version) {
|
|
localStorage.setItem(VERSION_STORAGE_KEY, version)
|
|
}
|
|
|
|
sessionStorage.removeItem(RETRY_KEY)
|
|
sessionStorage.removeItem(RECOVERY_KEY)
|
|
|
|
reloadWithCacheBust()
|
|
}
|
|
|
|
window.__APP_BOOT_SIMPLE_RETRY__ = function () {
|
|
sessionStorage.setItem(RETRY_KEY, '1')
|
|
reloadWithCacheBust()
|
|
}
|
|
|
|
window.__APP_BOOT_CLEAN_RELOAD__ = async function () {
|
|
try {
|
|
sessionStorage.setItem(RECOVERY_KEY, '1')
|
|
await cleanFetchAndReload()
|
|
} catch (error) {
|
|
console.error('Manual boot recovery failed:', error)
|
|
renderBootError()
|
|
}
|
|
}
|
|
|
|
window.__BOOT_TIMEOUT__ = window.setTimeout(async function () {
|
|
if (window.__APP_BOOTSTRAPPED__) return
|
|
|
|
var app = document.getElementById('app')
|
|
var stillBooting = app && app.querySelector('.boot-loader')
|
|
|
|
if (!stillBooting) return
|
|
|
|
if (!sessionStorage.getItem(RETRY_KEY)) {
|
|
renderBootError()
|
|
return
|
|
}
|
|
|
|
if (!sessionStorage.getItem(RECOVERY_KEY)) {
|
|
sessionStorage.setItem(RECOVERY_KEY, '1')
|
|
|
|
try {
|
|
await cleanFetchAndReload()
|
|
} catch (error) {
|
|
console.error('Auto boot recovery after retry failed:', error)
|
|
renderBootError()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
renderBootError()
|
|
}, BOOT_TIMEOUT_MS)
|
|
})()
|
|
</script>
|
|
|
|
<script type="module" src="/src/main.ts"></script>
|
|
</body>
|
|
</html> |