Files
ghatreh_user/index.html
T
mohmdsdgandCursor bddd591d63 feat(boot): use boot-loader as splash with cached tenant logo
Remove the Vue splash overlay and unused duplicate public logos from dist output.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 20:24:44 +03:30

847 lines
25 KiB
HTML

<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<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"
/>
<title>در حال بارگذاری...</title>
<!---->
<style>
:root {
--boot-bg: #ffffff;
--boot-bg-soft: #f8fafc;
--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%;
background: var(--boot-bg);
}
#app {
min-height: 100vh;
}
.boot-loader {
position: fixed;
inset: 0;
z-index: 2147483646;
width: 100%;
min-height: 100%;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
direction: rtl;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background:
radial-gradient(ellipse 70% 45% at 50% 28%, var(--boot-orb), transparent 70%),
linear-gradient(180deg, var(--boot-bg) 0%, var(--boot-bg-soft) 100%);
color: var(--boot-text);
overflow: hidden;
}
.boot-loader__logo {
height: min(20vh, 160px);
width: auto;
max-width: min(70vw, 280px);
object-fit: contain;
opacity: 0;
transition: opacity 0.28s ease;
}
.boot-loader__logo.is-ready {
opacity: 1;
}
.boot-loader__spinner {
position: absolute;
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
width: 26px;
height: 26px;
border-radius: 999px;
border: 3px solid var(--boot-spinner-track);
border-top-color: var(--boot-accent);
animation: boot-spin 0.9s linear infinite;
}
@keyframes boot-spin {
to {
transform: rotate(360deg);
}
}
</style>
<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) {
var decoded = decodeURIComponent(rawCookie)
var payload = JSON.parse(decoded)
if (payload && payload.config) return payload.config
}
} catch (error) {
console.warn('Boot theme: failed to read cached config', error)
}
try {
var rawLs = localStorage.getItem('APP_CONFIG_LS_CACHE')
if (!rawLs) return null
var lsPayload = JSON.parse(rawLs)
if (lsPayload && lsPayload.config) return lsPayload.config
} catch (error) {
console.warn('Boot theme: failed to read LS config cache', 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)
}
var BOOT_LOGO_KEY = 'BOOT_LOGO_URL'
var BOOT_LOGO_VERSION_KEY = 'BOOT_LOGO_VERSION'
function pickAvatarRaw(value) {
if (value == null) return ''
if (typeof value === 'string') return String(value).trim()
if (typeof value === 'object') {
return String(value.full_src || value.url || value.src || value.avatar || '').trim()
}
return ''
}
function logoCacheValid() {
try {
var v = String(localStorage.getItem('version') || '').trim()
var lv = String(localStorage.getItem(BOOT_LOGO_VERSION_KEY) || '').trim()
if (v && lv && v !== lv) return false
return true
} catch (e) {
return false
}
}
function readCachedLogoUrl() {
if (!logoCacheValid()) return ''
try {
return String(localStorage.getItem(BOOT_LOGO_KEY) || '').trim()
} catch (e) {
return ''
}
}
function resolveAbsoluteLogoUrl(raw) {
if (!raw) return ''
if (/^(https?:\/\/|data:|blob:)/i.test(raw)) {
return window.location.protocol === 'https:' ? raw.replace(/^http:\/\//i, 'https://') : raw
}
var path = raw.replace(/^\/+/, '')
return window.location.origin + '/server/' + path
}
function resolveBootLogoUrl(config) {
var cached = readCachedLogoUrl()
if (cached && /^(data:|blob:)/i.test(cached)) return cached
if (cached && /^(https?:\/\/)/i.test(cached)) return resolveAbsoluteLogoUrl(cached)
var nested = config && config.user && typeof config.user === 'object' ? config.user : {}
var root = config && typeof config === 'object' ? config : {}
var raw = pickAvatarRaw(nested.avatar) || pickAvatarRaw(root.avatar) || cached
return resolveAbsoluteLogoUrl(raw)
}
function persistLogoCache(logoUrl) {
try {
var v = String(localStorage.getItem('version') || '').trim()
if (v) localStorage.setItem(BOOT_LOGO_VERSION_KEY, v)
if (!logoUrl) return
localStorage.setItem(BOOT_LOGO_KEY, logoUrl)
if (logoUrl.indexOf('data:') === 0 || typeof fetch !== 'function') return
fetch(logoUrl, { mode: 'cors', credentials: 'omit' })
.then(function (res) {
return res && res.ok ? res.blob() : Promise.reject()
})
.then(function (blob) {
if (!blob || blob.size > 450000) return
var reader = new FileReader()
reader.onloadend = function () {
try {
if (typeof reader.result === 'string' && reader.result.indexOf('data:') === 0) {
localStorage.setItem(BOOT_LOGO_KEY, reader.result)
}
} catch (e) {}
}
reader.readAsDataURL(blob)
})
.catch(function () {})
} catch (e) {}
}
function applyBootLogo(config) {
var img = document.getElementById('boot-logo')
if (!img) return
var logoUrl = resolveBootLogoUrl(config || {})
if (!logoUrl) {
img.hidden = true
img.removeAttribute('src')
img.classList.remove('is-ready')
return
}
function markReady() {
img.hidden = false
img.removeAttribute('hidden')
img.classList.add('is-ready')
persistLogoCache(logoUrl)
}
img.onload = markReady
img.onerror = function () {
img.hidden = true
img.classList.remove('is-ready')
}
if (img.getAttribute('src') === logoUrl && img.complete && img.naturalWidth > 0) {
markReady()
return
}
img.hidden = false
img.removeAttribute('hidden')
img.src = logoUrl
if (logoUrl.indexOf('data:') === 0) markReady()
}
window.__APPLY_BOOT_BRANDING__ = applyBootLogo
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)
document.title = user.name_fa || user.name || 'در حال بارگذاری...'
}
applyBootTheme()
applyBootLogo(readCachedConfig())
})()
</script>
</head>
<body>
<div id="app">
<div class="boot-loader">
<img id="boot-logo" class="boot-loader__logo" alt="" decoding="async" hidden />
<div class="boot-loader__spinner" aria-hidden="true"></div>
</div>
</div>
<script>
if (typeof window.__APPLY_BOOT_BRANDING__ === 'function') {
window.__APPLY_BOOT_BRANDING__()
}
</script>
<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 = 'liman_user_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 isDevHost() {
var host = String(window.location.hostname || '').trim()
return host === 'localhost' || host === '127.0.0.1'
}
function getVersionUrls() {
var domain = encodeURIComponent(resolveTenantDomain())
var urls = getSuperAdminBases().map(function (base) {
return base + '/get_version?domain=' + domain
})
if (isDevHost()) {
var protocol = getProtocol()
urls = urls.concat([
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
protocol + '//212.23.201.173:8381/get_version',
protocol + '//91.212.174.137:4040/server/get_version'
])
}
return urls
}
function getConfigUrls() {
var domain = encodeURIComponent(resolveTenantDomain())
var urls = getSuperAdminBases().map(function (base) {
return base + '/get_user?domain=' + domain
})
if (isDevHost()) {
var protocol = getProtocol()
urls = urls.concat([
protocol + '//tablo.talasevin.ir/get_user',
protocol + '//abshode-service.montgit.ir/server/get_user',
protocol + '//212.23.201.173:8381/get_user',
protocol + '//91.212.174.137:4040/server/get_user'
])
}
return urls
}
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)
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
if (typeof window.__APPLY_BOOT_BRANDING__ === 'function') {
window.__APPLY_BOOT_BRANDING__(config)
}
} 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 clearLocalStorageExceptToken() {
var token = localStorage.getItem(TOKEN_STORAGE_KEY)
localStorage.clear()
if (token) {
localStorage.setItem(TOKEN_STORAGE_KEY, token)
}
}
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>