feat: load program-settings from SuperAdmin with 3-host failover
Use /api/public/program-settings only and retry montzar/montech/montest three rounds before showing the bootstrap error. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+31
-18
@@ -341,34 +341,47 @@
|
||||
]
|
||||
}
|
||||
|
||||
function isDevHost() {
|
||||
var host = String(window.location.hostname || '').trim()
|
||||
return host === 'localhost' || host === '127.0.0.1'
|
||||
}
|
||||
|
||||
function getVersionUrls() {
|
||||
var protocol = getProtocol()
|
||||
var domain = encodeURIComponent(resolveTenantDomain())
|
||||
var urls = getSuperAdminBases().map(function (base) {
|
||||
return base + '/get_version?domain=' + domain
|
||||
})
|
||||
|
||||
return 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'
|
||||
])
|
||||
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 protocol = getProtocol()
|
||||
var domain = encodeURIComponent(resolveTenantDomain())
|
||||
var urls = getSuperAdminBases().map(function (base) {
|
||||
return base + '/get_user?domain=' + domain
|
||||
})
|
||||
|
||||
return 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'
|
||||
])
|
||||
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) {
|
||||
@@ -646,10 +659,10 @@
|
||||
'<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;">' +
|
||||
'پاکسازی و تلاش مجدد' +
|
||||
@@ -661,8 +674,8 @@
|
||||
async function cleanFetchAndReload() {
|
||||
renderRecovering('در حال دریافت تنظیمات جدید برنامه...')
|
||||
|
||||
var version = await loadRemoteVersion(5)
|
||||
var freshConfigResult = await fetchFreshConfig(5)
|
||||
var version = await loadRemoteVersion(3)
|
||||
var freshConfigResult = await fetchFreshConfig(3)
|
||||
|
||||
clearReadableCookies()
|
||||
// clearLocalStorageExceptToken()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
fetchSuperAdminJson,
|
||||
resolveTenantDomain,
|
||||
SA_PROGRAM_SETTINGS_PATH,
|
||||
} from '@/config/superadmin'
|
||||
|
||||
export type SuperAdminSettingRow = {
|
||||
id?: number
|
||||
key?: string
|
||||
value?: string | number
|
||||
title_fa?: string
|
||||
description?: string
|
||||
is_active?: boolean | number | string
|
||||
type?: string
|
||||
can_edit?: number | boolean | string
|
||||
}
|
||||
|
||||
function parseSettingsPayload(data: unknown): SuperAdminSettingRow[] {
|
||||
if (Array.isArray(data)) return data as SuperAdminSettingRow[]
|
||||
if (data && typeof data === 'object') {
|
||||
const obj = data as Record<string, unknown>
|
||||
if (Array.isArray(obj.settings)) return obj.settings as SuperAdminSettingRow[]
|
||||
if (Array.isArray(obj.items)) return obj.items as SuperAdminSettingRow[]
|
||||
if (Array.isArray(obj.data)) return obj.data as SuperAdminSettingRow[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** تنظیمات پیشرفته نسخه از سوپرادمین — فقط GET /api/public/program-settings?domain= */
|
||||
export async function fetchSuperAdminProgramSettings(): Promise<SuperAdminSettingRow[]> {
|
||||
const domain = encodeURIComponent(resolveTenantDomain())
|
||||
try {
|
||||
const data = await fetchSuperAdminJson(
|
||||
(base) => `${base}${SA_PROGRAM_SETTINGS_PATH}?domain=${domain}`,
|
||||
)
|
||||
return parseSettingsPayload(data)
|
||||
} catch (error) {
|
||||
console.warn('SuperAdmin program-settings failed after retries:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -48,13 +48,13 @@ function showBootstrapError(error: unknown) {
|
||||
<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, rgba(15,23,42,0.62)); line-height: 2; font-size: 14px;">
|
||||
لطفاً اتصال اینترنت خود را بررسی کنید و دوباره تلاش کنید.
|
||||
برقراری ارتباط با سرور امکانپذیر نیست. لطفاً اتصال اینترنت خود را بررسی کرده و صفحه را تازهسازی نمایید.
|
||||
</p>
|
||||
<button
|
||||
onclick="location.reload()"
|
||||
style="padding: 11px 22px; border: 0; border-radius: 12px; background: var(--boot-accent, #2563eb); color: white; cursor: pointer; font-weight: 700;"
|
||||
>
|
||||
تلاش مجدد
|
||||
تازهسازی صفحه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+38
-25
@@ -1,44 +1,49 @@
|
||||
import type { AppConfig } from '@/config/types'
|
||||
import { normalizeAppConfig } from '@/config/app-config'
|
||||
import { readCachedAppConfig, writeCachedAppConfig } from '@/config/config-cache'
|
||||
import {
|
||||
allowSuperAdminFallback,
|
||||
getSuperAdminBase,
|
||||
getSuperAdminBases,
|
||||
isSuperAdminUrl,
|
||||
resolveTenantDomain,
|
||||
SA_RETRY_ROUNDS,
|
||||
} from '@/config/superadmin'
|
||||
|
||||
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
||||
export const CONFIG_RETRY_ROUNDS = 5
|
||||
|
||||
const SUPERADMIN_BASES = [
|
||||
'https://adm.montzar.ir',
|
||||
'https://adm.montech.ir',
|
||||
'https://adm.montest.ir',
|
||||
] as const
|
||||
export const CONFIG_RETRY_ROUNDS = SA_RETRY_ROUNDS
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function getConfigUrls() {
|
||||
const protocol = getProtocol()
|
||||
const domain = encodeURIComponent(resolveTenantDomain())
|
||||
const urls = getSuperAdminBases().map((base) => `${base}/get_user?domain=${domain}`)
|
||||
|
||||
return [
|
||||
...SUPERADMIN_BASES.map((base) => `${base}/get_user?domain=${domain}`),
|
||||
`${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`,
|
||||
]
|
||||
if (allowSuperAdminFallback()) {
|
||||
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
|
||||
}
|
||||
|
||||
export function sortConfigUrls(urls: string[]) {
|
||||
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
||||
if (!lastSuccessUrl || !urls.includes(lastSuccessUrl)) {
|
||||
if (
|
||||
!lastSuccessUrl ||
|
||||
!urls.includes(lastSuccessUrl) ||
|
||||
!isSuperAdminUrl(lastSuccessUrl)
|
||||
) {
|
||||
if (lastSuccessUrl && !isSuperAdminUrl(lastSuccessUrl)) {
|
||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
||||
}
|
||||
return [...urls]
|
||||
}
|
||||
return [lastSuccessUrl, ...urls.filter((url) => url !== lastSuccessUrl)]
|
||||
@@ -91,7 +96,11 @@ export async function fetchFreshAppConfig(rounds = CONFIG_RETRY_ROUNDS): Promise
|
||||
const timeout = index === urls.length - 1 ? 7000 : 4000
|
||||
const config = await fetchConfig(url, timeout)
|
||||
|
||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
||||
if (isSuperAdminUrl(url)) {
|
||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
||||
} else {
|
||||
clearStaleConfigSuccessUrl(url)
|
||||
}
|
||||
writeCachedAppConfig(config)
|
||||
|
||||
return config
|
||||
@@ -102,7 +111,11 @@ export async function fetchFreshAppConfig(rounds = CONFIG_RETRY_ROUNDS): Promise
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Cannot load app config from any server')
|
||||
throw new Error(
|
||||
getSuperAdminBase()
|
||||
? `Cannot load from SuperAdmin (${getSuperAdminBase()}).`
|
||||
: 'Cannot load app config from any server',
|
||||
)
|
||||
}
|
||||
|
||||
export async function loadAppConfig(): Promise<AppConfig> {
|
||||
|
||||
+45
-41
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
allowSuperAdminFallback,
|
||||
getSuperAdminBases,
|
||||
isSuperAdminUrl,
|
||||
resolveTenantDomain,
|
||||
SA_RETRY_ROUNDS,
|
||||
} from '@/config/superadmin'
|
||||
|
||||
type VersionResponse =
|
||||
| string
|
||||
| number
|
||||
@@ -9,10 +17,9 @@ type VersionResponse =
|
||||
}
|
||||
|
||||
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
|
||||
export const VERSION_RETRY_ROUNDS = 5
|
||||
export const VERSION_RETRY_ROUNDS = SA_RETRY_ROUNDS
|
||||
|
||||
let versionFetchPromise: Promise<string | null> | null = null
|
||||
let cachedVersionResult: string | null | undefined
|
||||
|
||||
function getProtocol() {
|
||||
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
||||
@@ -20,31 +27,34 @@ function getProtocol() {
|
||||
|
||||
function getVersionUrls() {
|
||||
const protocol = getProtocol()
|
||||
const host = String(window.location.hostname || '').trim()
|
||||
const domain = encodeURIComponent(
|
||||
!host || host === 'localhost' || host === '127.0.0.1' ? 'k.montzar.ir' : host,
|
||||
)
|
||||
const domain = encodeURIComponent(resolveTenantDomain())
|
||||
const urls = getSuperAdminBases().map((base) => `${base}/get_version?domain=${domain}`)
|
||||
|
||||
return [
|
||||
`https://adm.montzar.ir/get_version?domain=${domain}`,
|
||||
`https://adm.montech.ir/get_version?domain=${domain}`,
|
||||
`https://adm.montest.ir/get_version?domain=${domain}`,
|
||||
'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`,
|
||||
]
|
||||
if (allowSuperAdminFallback()) {
|
||||
urls.push(
|
||||
'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 sortVersionUrls(urls: string[]) {
|
||||
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
|
||||
|
||||
if (!lastSuccessUrl) {
|
||||
return urls
|
||||
if (
|
||||
!lastSuccessUrl ||
|
||||
!urls.includes(lastSuccessUrl) ||
|
||||
!isSuperAdminUrl(lastSuccessUrl)
|
||||
) {
|
||||
if (lastSuccessUrl && !isSuperAdminUrl(lastSuccessUrl)) {
|
||||
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
||||
}
|
||||
return [...urls]
|
||||
}
|
||||
|
||||
const others = urls.filter((url) => url !== lastSuccessUrl)
|
||||
return [lastSuccessUrl, ...others]
|
||||
return [lastSuccessUrl, ...urls.filter((url) => url !== lastSuccessUrl)]
|
||||
}
|
||||
|
||||
function clearStaleVersionSuccessUrl(url: string) {
|
||||
@@ -95,10 +105,10 @@ async function fetchVersion(url: string, timeout = 3500): Promise<string | null>
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
Accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -106,18 +116,22 @@ async function fetchVersion(url: string, timeout = 3500): Promise<string | null>
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw new Error('Version response is not JSON')
|
||||
}
|
||||
|
||||
const data = contentType.includes('application/json')
|
||||
? ((await response.json()) as VersionResponse)
|
||||
: ((await response.text()) as VersionResponse)
|
||||
|
||||
const data = (await response.json()) as VersionResponse
|
||||
const version = normalizeVersionResponse(data)
|
||||
|
||||
if (!version) {
|
||||
throw new Error('Version response is empty')
|
||||
}
|
||||
|
||||
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
|
||||
if (isSuperAdminUrl(url)) {
|
||||
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
|
||||
} else {
|
||||
clearStaleVersionSuccessUrl(url)
|
||||
}
|
||||
|
||||
return version
|
||||
} finally {
|
||||
@@ -149,7 +163,6 @@ async function fetchVersionWithRetry(rounds = VERSION_RETRY_ROUNDS): Promise<str
|
||||
}
|
||||
|
||||
export function resetVersionCheckCache() {
|
||||
cachedVersionResult = undefined
|
||||
versionFetchPromise = null
|
||||
}
|
||||
|
||||
@@ -164,22 +177,13 @@ export async function loadRemoteAppVersion(options?: {
|
||||
resetVersionCheckCache()
|
||||
}
|
||||
|
||||
if (cachedVersionResult !== undefined) {
|
||||
return cachedVersionResult
|
||||
}
|
||||
|
||||
if (versionFetchPromise) {
|
||||
return versionFetchPromise
|
||||
}
|
||||
|
||||
versionFetchPromise = fetchVersionWithRetry(rounds)
|
||||
.then((version) => {
|
||||
cachedVersionResult = version
|
||||
return version
|
||||
})
|
||||
.finally(() => {
|
||||
versionFetchPromise = null
|
||||
})
|
||||
versionFetchPromise = fetchVersionWithRetry(rounds).finally(() => {
|
||||
versionFetchPromise = null
|
||||
})
|
||||
|
||||
return versionFetchPromise
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/** سوپرادمین — مستر، بعد دو اسلیو */
|
||||
export const SUPERADMIN_BASES = [
|
||||
'https://adm.montzar.ir',
|
||||
'https://adm.montech.ir',
|
||||
'https://adm.montest.ir',
|
||||
] as const
|
||||
|
||||
/** هر API: سه دامنه × سه دور */
|
||||
export const SA_RETRY_ROUNDS = 3
|
||||
|
||||
/** تنظیمات پیشرفته نسخه — همان دادهٔ /get_settings بدون نیاز به nginx جدا */
|
||||
export const SA_PROGRAM_SETTINGS_PATH = '/api/public/program-settings'
|
||||
|
||||
function normalizeBase(url: string) {
|
||||
return String(url || '').trim().replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export function getSuperAdminBases(): string[] {
|
||||
const fromEnv = normalizeBase(String(import.meta.env.VITE_SUPERADMIN_URL || ''))
|
||||
const bases = [...SUPERADMIN_BASES]
|
||||
if (fromEnv) {
|
||||
return [fromEnv, ...bases.filter((base) => base !== fromEnv)]
|
||||
}
|
||||
return bases
|
||||
}
|
||||
|
||||
export function getSuperAdminBase(): string {
|
||||
return getSuperAdminBases()[0] || ''
|
||||
}
|
||||
|
||||
export function resolveTenantDomain(): string {
|
||||
const override = String(import.meta.env.VITE_TENANT_DOMAIN || '').trim()
|
||||
if (override) return override
|
||||
|
||||
const host = String(window.location.hostname || '').trim()
|
||||
if (!host || host === 'localhost' || host === '127.0.0.1') {
|
||||
return 'k.montzar.ir'
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
export function isSuperAdminUrl(url: string): boolean {
|
||||
return getSuperAdminBases().some((base) => url.startsWith(base))
|
||||
}
|
||||
|
||||
export function allowSuperAdminFallback(): boolean {
|
||||
return Boolean(import.meta.env.DEV)
|
||||
}
|
||||
|
||||
export async function fetchSuperAdminJson<T>(
|
||||
buildUrl: (base: string) => string,
|
||||
timeout = 5000,
|
||||
): Promise<T> {
|
||||
let lastError: unknown
|
||||
const bases = getSuperAdminBases()
|
||||
for (let round = 1; round <= SA_RETRY_ROUNDS; round++) {
|
||||
for (const base of bases) {
|
||||
const controller = new AbortController()
|
||||
const timer = window.setTimeout(() => controller.abort(), timeout)
|
||||
try {
|
||||
const res = await fetch(buildUrl(base), {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
const contentType = res.headers.get('content-type') || ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw new Error('SuperAdmin response is not JSON')
|
||||
}
|
||||
return (await res.json()) as T
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
} finally {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError instanceof Error ? lastError : new Error('SuperAdmin unreachable')
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getSettings } from '@/api/configuration'
|
||||
import { fetchSuperAdminProgramSettings } from '@/api/superadminSettings'
|
||||
import { resolveMeltedGoldLabel, resolveMetalTypeLabel } from '@/utils/metalLabels'
|
||||
import { resolveTradeWeightUnit } from '@/utils/weightUnit'
|
||||
|
||||
@@ -65,9 +66,15 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
|
||||
fetchPromise = (async () => {
|
||||
try {
|
||||
const data = await getSettings()
|
||||
if (Array.isArray(data)) {
|
||||
mergeSettings(data)
|
||||
const [local, remote] = await Promise.all([
|
||||
getSettings().catch(() => []),
|
||||
fetchSuperAdminProgramSettings(),
|
||||
])
|
||||
if (Array.isArray(local)) {
|
||||
mergeSettings(local)
|
||||
}
|
||||
if (remote.length) {
|
||||
mergeSettings(remote as AppSetting[])
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
Reference in New Issue
Block a user