Files
boursecu_admin/src/config-loader.js
T
mohmdsdgandCursor 509d26d04b fix(admin): cap get_version and get_user waits at 10s
Keep the longer overlay window, but stop waiting 20–25s per bootstrap request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 21:56:52 +03:30

662 lines
20 KiB
JavaScript

(function() {
'use strict';
window.__APP_CONFIG__ = window.__APP_CONFIG__ || {};
window.__CONFIG_READY__ = false;
let configResolve;
let configReject;
window.__CONFIG_PROMISE__ = new Promise((resolve, reject) => {
configResolve = resolve;
configReject = reject;
});
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL';
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
const CONFIG_CACHE_KEY = 'APP_CONFIG_CACHE';
const CONFIG_IDB_NAME = 'abshode_admin_cfg';
const CONFIG_IDB_STORE = 'kv';
const CONFIG_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
const VERSION_STORAGE_KEY = 'version';
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock';
const VERSION_LAST_RELOAD_KEY = 'version_last_reload';
const AUTH_TOKEN_KEY = 'peransa_token';
const VERSION_RETRY_ROUNDS = 3;
const CONFIG_RETRY_ROUNDS = 3;
const VERSION_FETCH_TIMEOUT_MS = 10000;
const CONFIG_FETCH_TIMEOUT_MS = 10000;
const CONFIG_TIMEOUT = 60000;
let configTimeoutId;
let mainScriptLoaded = false;
function getProtocol() {
return window.location.protocol === 'https:' ? 'https:' : 'http:';
}
function resolveTenantDomain() {
const host = String(window.location.hostname || '').trim();
if (!host || host === 'localhost' || host === '127.0.0.1') {
return 'k.montzar.ir';
}
return host;
}
function isDevHost() {
const host = String(window.location.hostname || '').trim();
return host === 'localhost' || host === '127.0.0.1';
}
function getVersionUrls() {
const domain = encodeURIComponent(resolveTenantDomain());
const urls = [
'https://adm.montzar.ir/get_version?domain=' + domain,
'https://adm.montech.ir/get_version?domain=' + domain,
'https://adm.montest.ir/get_version?domain=' + domain
];
if (isDevHost()) {
urls.push(
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
getProtocol() + '//212.23.201.173:8381/get_version',
getProtocol() + '//91.212.174.137:4040/server/get_version'
);
}
return urls;
}
function getConfigUrls() {
const protocol = getProtocol();
const domain = encodeURIComponent(resolveTenantDomain());
const urls = [
'https://adm.montzar.ir/get_user?domain=' + domain,
'https://adm.montech.ir/get_user?domain=' + domain,
'https://adm.montest.ir/get_user?domain=' + domain
];
if (isDevHost()) {
urls.push(
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 sortUrls(urls, successKey) {
const lastSuccessUrl = localStorage.getItem(successKey);
if (!lastSuccessUrl) return urls.slice();
return [lastSuccessUrl].concat(urls.filter((url) => url !== lastSuccessUrl));
}
function clearStaleSuccessUrl(key, url) {
if (localStorage.getItem(key) === url) {
localStorage.removeItem(key);
}
}
function normalizeVersion(value) {
if (value === null || value === undefined) return '';
return String(value).trim();
}
function readUserVersion(user) {
if (!user || typeof user !== 'object' || Array.isArray(user)) return '';
const version = user.version ?? user.value;
return version === undefined || version === null ? '' : String(version).trim();
}
function normalizeVersionResponse(response) {
if (typeof response === 'string' || typeof response === 'number') {
return String(response).trim();
}
if (!response || typeof response !== 'object') return '';
if (typeof response.data === 'string' || typeof response.data === 'number') {
return String(response.data).trim();
}
const directVersion =
response.version ??
response.value ??
(response.data && typeof response.data === 'object'
? response.data.version ?? response.data.value
: undefined);
if (directVersion !== undefined && directVersion !== null) {
return String(directVersion).trim();
}
return readUserVersion(response.user);
}
function readCachedAppConfigPayload() {
try {
const raw = localStorage.getItem(CONFIG_CACHE_KEY);
if (!raw) return null;
const payload = JSON.parse(raw);
if (!payload || typeof payload !== 'object' || !payload.config) return null;
return payload;
} catch (error) {
console.warn('Failed to read cached get_user:', error);
localStorage.removeItem(CONFIG_CACHE_KEY);
return null;
}
}
function readCachedAppConfig() {
const payload = readCachedAppConfigPayload();
return payload && payload.config ? payload.config : null;
}
function isFreshGetUserCache(payload, remoteVersion, domain) {
if (!payload || !payload.config) return false;
if (isServerUnreachable(payload.config)) return false;
const cachedDomain = String(payload.domain || '').trim();
if (cachedDomain && cachedDomain !== domain) return false;
const age = Date.now() - Number(payload.cached_at || 0);
if (!payload.cached_at || age < 0 || age > CONFIG_CACHE_MAX_AGE_MS) return false;
const cachedVersion = normalizeVersion(payload.version);
if (remoteVersion) {
return cachedVersion === remoteVersion;
}
return Boolean(cachedVersion);
}
function openConfigIdb() {
return new Promise(function (resolve, reject) {
if (!window.indexedDB) {
reject(new Error('indexedDB unavailable'));
return;
}
const req = window.indexedDB.open(CONFIG_IDB_NAME, 1);
req.onupgradeneeded = function () {
if (!req.result.objectStoreNames.contains(CONFIG_IDB_STORE)) {
req.result.createObjectStore(CONFIG_IDB_STORE);
}
};
req.onsuccess = function () {
resolve(req.result);
};
req.onerror = function () {
reject(req.error);
};
});
}
function writeCachedAppConfigToIdb(payload) {
openConfigIdb()
.then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(CONFIG_IDB_STORE, 'readwrite');
tx.oncomplete = function () {
resolve();
};
tx.onerror = function () {
reject(tx.error);
};
tx.objectStore(CONFIG_IDB_STORE).put(payload, CONFIG_CACHE_KEY);
});
})
.catch(function (error) {
console.warn('Failed to write get_user IndexedDB cache:', error);
});
}
function readCachedAppConfigFromIdb() {
return openConfigIdb()
.then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(CONFIG_IDB_STORE, 'readonly');
const req = tx.objectStore(CONFIG_IDB_STORE).get(CONFIG_CACHE_KEY);
req.onsuccess = function () {
const value = req.result;
if (!value || typeof value !== 'object' || !value.config) {
resolve(null);
return;
}
resolve(value);
};
req.onerror = function () {
reject(req.error);
};
});
})
.catch(function () {
return null;
});
}
function clearCachedAppConfigIdb() {
openConfigIdb()
.then(function (db) {
const tx = db.transaction(CONFIG_IDB_STORE, 'readwrite');
tx.objectStore(CONFIG_IDB_STORE).delete(CONFIG_CACHE_KEY);
})
.catch(function () {
/* ignore */
});
}
function writeCachedAppConfig(config, version) {
const payload = {
cached_at: Date.now(),
version: normalizeVersion(version || getStoredAppVersion()),
domain: resolveTenantDomain(),
config: config
};
try {
localStorage.setItem(CONFIG_CACHE_KEY, JSON.stringify(payload));
clearCachedAppConfigIdb();
} catch (error) {
console.warn('get_user too large for localStorage; using IndexedDB', error);
try {
localStorage.removeItem(CONFIG_CACHE_KEY);
} catch {
/* ignore */
}
writeCachedAppConfigToIdb(payload);
}
}
async function resolveCachedPayload() {
const fromLs = readCachedAppConfigPayload();
if (fromLs) return fromLs;
return await readCachedAppConfigFromIdb();
}
function getStoredAppVersion() {
return localStorage.getItem(VERSION_STORAGE_KEY);
}
function writeStoredAppVersion(version) {
localStorage.setItem(VERSION_STORAGE_KEY, normalizeVersion(version));
}
function shouldUpdateForVersion(serverVersion) {
const normalized = normalizeVersion(serverVersion);
if (!normalized) return false;
const stored = getStoredAppVersion();
if (!stored) {
writeStoredAppVersion(normalized);
return false;
}
return stored !== normalized;
}
function canStartVersionUpdate(serverVersion) {
const version = normalizeVersion(serverVersion);
if (!version) return false;
if (sessionStorage.getItem(VERSION_LAST_RELOAD_KEY) === version) {
return false;
}
const rawLock = sessionStorage.getItem(VERSION_UPDATE_LOCK_KEY);
if (rawLock) {
try {
const lock = JSON.parse(rawLock);
const isSameVersion = lock.version === version;
const isFreshLock = Date.now() - lock.started_at < 60_000;
if (isSameVersion && isFreshLock) return false;
} catch {
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY);
}
}
return true;
}
function startVersionUpdateLock(serverVersion) {
sessionStorage.setItem(
VERSION_UPDATE_LOCK_KEY,
JSON.stringify({
version: normalizeVersion(serverVersion),
started_at: Date.now()
})
);
}
function finishVersionUpdateBeforeReload(serverVersion) {
const version = normalizeVersion(serverVersion);
writeStoredAppVersion(version);
sessionStorage.setItem(VERSION_LAST_RELOAD_KEY, version);
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY);
}
function cancelVersionUpdateLock() {
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY);
}
function preserveAuthToken() {
return localStorage.getItem(AUTH_TOKEN_KEY);
}
function restoreAuthToken(token) {
if (token) {
localStorage.setItem(AUTH_TOKEN_KEY, token);
}
}
function clearBootstrapCachesKeepToken() {
const token = preserveAuthToken();
localStorage.removeItem(CONFIG_CACHE_KEY);
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY);
localStorage.removeItem(VERSION_SUCCESS_URL_KEY);
clearCachedAppConfigIdb();
restoreAuthToken(token);
}
function hardReloadApp() {
const refreshUrl =
window.location.origin +
window.location.pathname +
'?__app_refresh=' +
Date.now() +
(window.location.hash || '');
window.location.replace(refreshUrl);
}
async function fetchJson(url, timeout) {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), timeout);
try {
const 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);
}
const contentType = response.headers.get('content-type') || '';
const text = await response.text();
const trimmed = text.trim();
if (contentType.includes('application/json') || trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
return JSON.parse(trimmed);
} catch (error) {
throw new Error('Response is not JSON. Received: ' + trimmed.substring(0, 100));
}
}
if (trimmed) {
return trimmed;
}
throw new Error('Response is empty');
} finally {
window.clearTimeout(timeoutId);
}
}
async function loadVersion(rounds) {
rounds = rounds || VERSION_RETRY_ROUNDS;
const urls = sortUrls(getVersionUrls(), VERSION_SUCCESS_URL_KEY);
for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) {
const url = urls[index];
try {
const data = await fetchJson(url, VERSION_FETCH_TIMEOUT_MS);
const version = normalizeVersionResponse(data);
if (!version) {
throw new Error('Version response is empty');
}
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url);
console.log('Version loaded from:', url, version);
return version;
} catch (error) {
clearStaleSuccessUrl(VERSION_SUCCESS_URL_KEY, url);
console.warn('Version failed from', url, 'round:', round, error);
}
}
}
console.warn('Version check failed from all endpoints');
return null;
}
async function fetchConfig(rounds, version) {
rounds = rounds || CONFIG_RETRY_ROUNDS;
const urls = sortUrls(getConfigUrls(), CONFIG_SUCCESS_URL_KEY);
for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) {
const url = urls[index];
try {
const config = await fetchJson(url, CONFIG_FETCH_TIMEOUT_MS);
if (!config || typeof config !== 'object') {
throw new Error('Config response is invalid');
}
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
writeCachedAppConfig(config, version);
return { config: config, url: url };
} catch (error) {
clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url);
console.warn('Config failed from', url, 'round:', round, error);
}
}
}
throw new Error('Cannot load config from any server');
}
function applyConfig(config) {
if (isServerUnreachable(config)) {
showServerUnavailable();
throw new Error('خطا در برقراری ارتباط با سرور');
}
window.__APP_CONFIG__ = config || {};
window.__CONFIG_READY__ = true;
if (config && config.user && config.user.name_fa) {
document.title = 'پنل مدیریت ' + '-' + config.user.name_fa;
}
if (config && config.user && config.user.avatar) {
const faviconUrl = config.user.avatar;
const existingIcons = document.querySelectorAll('link[rel*="icon"]');
existingIcons.forEach((icon) => icon.remove());
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/png';
link.href = faviconUrl + '?v=' + Date.now();
document.head.appendChild(link);
}
}
function isServerUnreachable(config) {
if (!config || typeof config !== 'object') return false;
if (config.server_reachable === false) return true;
if (config.user && config.user.server_reachable === false) return true;
return false;
}
function showServerUnavailable() {
if (configTimeoutId) {
clearTimeout(configTimeoutId);
configTimeoutId = null;
}
mainScriptLoaded = true; // جلوی timeout برای لود main
var appRoot = document.querySelector('#app') || document.body;
appRoot.innerHTML =
'<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;direction:rtl;font-family:sans-serif;padding:24px;background:#fff;color:#111827;">' +
'<div style="width:min(420px,100%);text-align:center;border:1px solid rgba(15,23,42,0.08);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:rgba(15,23,42,0.62);line-height:2;font-size:14px;">برقراری ارتباط با سرور امکان‌پذیر نیست. لطفاً اتصال اینترنت خود را بررسی کرده و صفحه را تازه‌سازی نمایید.</p>' +
'<button type="button" onclick="location.reload()" style="padding:11px 22px;border:0;border-radius:12px;background:#2563eb;color:#fff;cursor:pointer;font-weight:700;">تازه‌سازی صفحه</button>' +
'</div></div>';
}
function finishConfigLoad(config, error) {
if (configResolve) {
configResolve(config);
}
if (error) {
window.dispatchEvent(new CustomEvent('configError', { detail: error }));
} else {
window.dispatchEvent(new CustomEvent('configReady', { detail: config }));
}
if (configTimeoutId) {
clearTimeout(configTimeoutId);
configTimeoutId = null;
}
loadMainScript();
}
async function applyVersionUpdate(serverVersion) {
startVersionUpdateLock(serverVersion);
try {
const { config } = await fetchConfig(undefined, serverVersion);
if (isServerUnreachable(config)) {
writeCachedAppConfig(config, serverVersion);
showServerUnavailable();
cancelVersionUpdateLock();
return true;
}
clearBootstrapCachesKeepToken();
writeCachedAppConfig(config, serverVersion);
applyConfig(config);
finishVersionUpdateBeforeReload(serverVersion);
console.log('App version changed — hard refresh', serverVersion);
hardReloadApp();
return true;
} catch (error) {
console.error('Version update failed:', error);
cancelVersionUpdateLock();
// بعد از bump نسخه (مثلاً قفل سوپر) کش قدیمی reachable را قبول نکن
showServerUnavailable();
return true;
}
}
async function loadConfig() {
let versionChanged = false;
let needsFreshForVersion = false;
try {
const cachedPayload = await resolveCachedPayload();
const cachedConfig = cachedPayload && cachedPayload.config ? cachedPayload.config : null;
if (cachedConfig && isServerUnreachable(cachedConfig)) {
showServerUnavailable();
return;
}
const remoteVersion = await loadVersion();
needsFreshForVersion = Boolean(remoteVersion && shouldUpdateForVersion(remoteVersion));
versionChanged = Boolean(
needsFreshForVersion && canStartVersionUpdate(remoteVersion)
);
if (versionChanged) {
const updating = await applyVersionUpdate(remoteVersion);
if (updating) {
return;
}
}
const domain = resolveTenantDomain();
if (
!needsFreshForVersion &&
isFreshGetUserCache(cachedPayload, remoteVersion, domain)
) {
applyConfig(cachedPayload.config);
if (remoteVersion) {
writeStoredAppVersion(remoteVersion);
}
console.log('Config loaded from cache; get_user skipped');
finishConfigLoad(cachedPayload.config);
return;
}
const { config, url } = await fetchConfig(undefined, remoteVersion);
applyConfig(config);
writeCachedAppConfig(config, remoteVersion);
if (remoteVersion) {
writeStoredAppVersion(remoteVersion);
}
console.log('Config loaded successfully from', url, config);
finishConfigLoad(config);
} catch (error) {
if (error && String(error.message || '').includes('خطا در برقراری ارتباط با سرور')) {
showServerUnavailable();
return;
}
console.warn('Config loader: Could not fetch config from any endpoint');
console.warn('Error details:', error && error.message ? error.message : error);
if (versionChanged || needsFreshForVersion) {
showServerUnavailable();
return;
}
const cachedPayload = await resolveCachedPayload();
const cachedConfig = cachedPayload && cachedPayload.config ? cachedPayload.config : null;
if (cachedConfig) {
try {
applyConfig(cachedConfig);
finishConfigLoad(cachedConfig, error);
return;
} catch (blockedError) {
showServerUnavailable();
return;
}
}
showServerUnavailable();
}
}
function loadMainScript() {
if (mainScriptLoaded || document.querySelector('script[src="/src/main.ts"]')) {
return;
}
mainScriptLoaded = true;
import('/src/main.ts').then(() => {
console.log('Main script loaded and executed');
});
}
configTimeoutId = setTimeout(function() {
if (!mainScriptLoaded) {
console.warn('Config loader: Timeout reached.');
const cachedConfig = readCachedAppConfig();
if (isServerUnreachable(cachedConfig) || isServerUnreachable(window.__APP_CONFIG__)) {
showServerUnavailable();
return;
}
showServerUnavailable();
}
}, CONFIG_TIMEOUT);
loadConfig();
})();