fix: lock new.gholaktala.ir as Khanegi newTheme (userTheme)

Correct prior modular/Hamkar misclassification. Protect locked newTheme domains from bulk-modular brand overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 17:53:04 +03:30
co-authored by Cursor
parent 859530a669
commit dd4850d9fa
4 changed files with 181 additions and 23 deletions
+13 -23
View File
@@ -1,13 +1,13 @@
/** /**
* Apply Niazi-style modular brand colors to all Khanegi tenants * Apply Niazi-style modular brand colors to Khanegi tenants
* that are NOT newTheme (Aygin stays newTheme). * that are NOT locked newTheme (Aygin, new.gholaktala.ir, …).
* *
* Colors (from app.niazitala.ir): * Colors (from app.niazitala.ir):
* primary #0a1b37 (هدر) * primary #0a1b37 (هدر)
* secondary #e0be7f (اکسنت) * secondary #e0be7f (اکسنت)
* background #ffffff (پس‌زمینه) * background #ffffff (پس‌زمینه)
* *
* Also forces uiVariant=modular for non-Aygin Khanegi rows. * Also forces uiVariant=modular for non-protected Khanegi rows.
* *
* Dry-run: * Dry-run:
* KHANEGI_BRAND_DRY_RUN=1 npm run prisma:bulk-khanegi-modular-brand * KHANEGI_BRAND_DRY_RUN=1 npm run prisma:bulk-khanegi-modular-brand
@@ -19,6 +19,7 @@
* KHANEGI_BRAND_SOURCE_DOMAIN=app.niazitala.ir npm run prisma:bulk-khanegi-modular-brand * KHANEGI_BRAND_SOURCE_DOMAIN=app.niazitala.ir npm run prisma:bulk-khanegi-modular-brand
*/ */
import { KhanegiUiVariant, PrismaClient, ProductType } from '@prisma/client'; import { KhanegiUiVariant, PrismaClient, ProductType } from '@prisma/client';
import { isForcedKhanegiNewTheme } from './khanegi-variant-locks';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -42,20 +43,9 @@ function normalizeHost(value?: string | null) {
.split(':')[0]; .split(':')[0];
} }
function isAyginTenant(domain: string, domains: unknown, slug: string) { function isProtectedNewThemeTenant(domain: string, domains: unknown, slug: string) {
const hosts = new Set<string>(); // Aygin + explicit locks (e.g. new.gholaktala.ir)
const primary = normalizeHost(domain); return isForcedKhanegiNewTheme(domain, domains, slug);
if (primary) hosts.add(primary);
const extra = Array.isArray(domains) ? domains : [];
for (const d of extra) {
const n = normalizeHost(String(d));
if (n) hosts.add(n);
}
if (String(slug || '').toLowerCase().includes('aygin')) return true;
for (const h of hosts) {
if (h.includes('aygingold') || h === 'app.aygingold.ir') return true;
}
return false;
} }
function bumpAppVersion(current?: string | null): string { function bumpAppVersion(current?: string | null): string {
@@ -134,10 +124,10 @@ async function main() {
}); });
const targets = tenants.filter( const targets = tenants.filter(
(t) => !isAyginTenant(t.domain, t.domains, t.slug), (t) => !isProtectedNewThemeTenant(t.domain, t.domains, t.slug),
); );
const skippedAygin = tenants.filter((t) => const skippedNewTheme = tenants.filter((t) =>
isAyginTenant(t.domain, t.domains, t.slug), isProtectedNewThemeTenant(t.domain, t.domains, t.slug),
); );
console.log( console.log(
@@ -153,7 +143,7 @@ async function main() {
}, },
totalKhanegi: tenants.length, totalKhanegi: tenants.length,
targets: targets.length, targets: targets.length,
skippedAygin: skippedAygin.map((t) => ({ skippedNewTheme: skippedNewTheme.map((t) => ({
domain: t.domain, domain: t.domain,
uiVariant: t.uiVariant, uiVariant: t.uiVariant,
})), })),
@@ -204,8 +194,8 @@ async function main() {
updated += 1; updated += 1;
} }
// Ensure Aygin stays newTheme (do not touch colors unless variant wrong) // Ensure locked newTheme tenants stay newTheme
for (const t of skippedAygin) { for (const t of skippedNewTheme) {
if (t.uiVariant === KhanegiUiVariant.newTheme) continue; if (t.uiVariant === KhanegiUiVariant.newTheme) continue;
console.log( console.log(
JSON.stringify({ JSON.stringify({
+2
View File
@@ -30,6 +30,8 @@ const FORCE_KHANEGI: Array<{ domain?: string; name?: string; externalId?: number
{ domain: 'abaeigold.ir' }, { domain: 'abaeigold.ir' },
{ domain: 'niazitala.ir' }, { domain: 'niazitala.ir' },
{ domain: 'garoosigholaktala.ir' }, { domain: 'garoosigholaktala.ir' },
// Khanegi newTheme (userTheme front) — NOT modular, NOT Hamkar.
// Keep uiVariant=newTheme in DB; see prisma/khanegi-variant-locks.ts
{ domain: 'new.gholaktala.ir' }, { domain: 'new.gholaktala.ir' },
{ domain: 'kh.kashkolgold.ir' }, { domain: 'kh.kashkolgold.ir' },
{ domain: 'goldsun.zaarbox.ir' }, { domain: 'goldsun.zaarbox.ir' },
+53
View File
@@ -0,0 +1,53 @@
/**
* Khanegi tenants that MUST stay on userTheme (uiVariant=newTheme).
* Never force these to modular in bulk brand scripts.
*
* Frontend: KhanegiModular/userTheme
* API: /api/get_settings (NOT Hamkar /site/settings, NOT userModular-only assumptions)
*/
export const FORCE_KHANEGI_NEW_THEME_DOMAINS = [
'new.gholaktala.ir',
'app.aygingold.ir',
] as const
export function normalizeTenantHost(value?: string | null) {
if (!value) return ''
return String(value)
.trim()
.toLowerCase()
.replace(/^https?:\/\//, '')
.split('/')[0]
.split(':')[0]
}
export function collectTenantHosts(domain: string, domains: unknown, slug = '') {
const hosts = new Set<string>()
const primary = normalizeTenantHost(domain)
if (primary) hosts.add(primary)
const extra = Array.isArray(domains) ? domains : []
for (const d of extra) {
const n = normalizeTenantHost(String(d))
if (n) hosts.add(n)
}
const s = String(slug || '').toLowerCase()
if (s) hosts.add(s)
return hosts
}
export function isForcedKhanegiNewTheme(domain: string, domains: unknown, slug = '') {
const hosts = collectTenantHosts(domain, domains, slug)
const slugLower = String(slug || '').toLowerCase()
if (slugLower.includes('aygin') || slugLower.includes('gholak_new')) return true
for (const h of hosts) {
if (h === 'new.gholaktala.ir') return true
if (h.includes('aygingold') || h === 'app.aygingold.ir') return true
}
for (const locked of FORCE_KHANEGI_NEW_THEME_DOMAINS) {
const needle = normalizeTenantHost(locked)
if (hosts.has(needle)) return true
}
return false
}
@@ -0,0 +1,113 @@
import { PrismaClient } from '@prisma/client'
/**
* Restore new.gholaktala.ir as Khanegi + newTheme (front: userTheme).
*
* On production:
* cd /var/www/superadmin/api && git pull
* set -a && . ./.env && set +a
* node scripts/fix-gholaktala-to-khanegi-newtheme.mjs
* node scripts/fix-gholaktala-to-khanegi-newtheme.mjs --apply
*
* Then verify:
* curl -s 'https://adm.montzar.ir/get_user?domain=new.gholaktala.ir' | head -c 500
* Expect: "product":"khanegi" and "ui_variant":"newTheme"
*/
const prisma = new PrismaClient()
const APPLY = process.argv.includes('--apply')
const DOMAIN = 'new.gholaktala.ir'
function bumpAppVersion(current) {
const v = String(current || '1').trim() || '1'
if (/^\d+$/.test(v)) return String(Number(v) + 1)
const semver = v.match(/^(\d+)\.(\d+)\.(\d+)$/)
if (semver) return `${semver[1]}.${semver[2]}.${Number(semver[3]) + 1}`
return String(Date.now())
}
async function main() {
const tenants = await prisma.tenant.findMany({
where: {
OR: [
{ domain: { equals: DOMAIN, mode: 'insensitive' } },
{ domains: { string_contains: DOMAIN } },
{ domain: { contains: 'gholaktala', mode: 'insensitive' } },
{ slug: { contains: 'gholak', mode: 'insensitive' } },
],
},
select: {
id: true,
nameFa: true,
domain: true,
slug: true,
product: true,
uiVariant: true,
appVersion: true,
notes: true,
},
})
console.log('MATCHES', JSON.stringify(tenants, null, 2))
const tenant =
tenants.find((t) => String(t.domain).toLowerCase() === DOMAIN) || tenants[0]
if (!tenant) throw new Error(`Tenant not found for ${DOMAIN}`)
const alreadyOk = tenant.product === 'khanegi' && tenant.uiVariant === 'newTheme'
console.log('SELECTED', {
id: tenant.id,
domain: tenant.domain,
nameFa: tenant.nameFa,
product: tenant.product,
uiVariant: tenant.uiVariant,
alreadyOk,
expected: {
product: 'khanegi',
ui_variant: 'newTheme',
frontend: 'KhanegiModular/userTheme (NOT userModular, NOT Hamkar)',
api: '/server/api/get_settings',
},
})
if (!APPLY) {
console.log('DRY_RUN — re-run with --apply to set product=khanegi uiVariant=newTheme')
return
}
const updated = await prisma.tenant.update({
where: { id: tenant.id },
data: {
product: 'khanegi',
uiVariant: 'newTheme',
appVersion: alreadyOk ? tenant.appVersion : bumpAppVersion(tenant.appVersion),
notes: [
String(tenant.notes || '')
.replace(/\s*\|\s*product=hamkar[^\|]*/gi, '')
.replace(/\s*\|\s*product=khanegi[^\|]*/gi, '')
.trim(),
'product=khanegi uiVariant=newTheme (userTheme locked)',
]
.filter(Boolean)
.join(' | '),
},
})
console.log('UPDATED', {
id: updated.id,
domain: updated.domain,
product: updated.product,
uiVariant: updated.uiVariant,
appVersion: updated.appVersion,
})
}
main()
.catch((e) => {
console.error(e)
process.exitCode = 1
})
.finally(async () => {
await prisma.$disconnect()
})