467 lines
21 KiB
TypeScript
467 lines
21 KiB
TypeScript
import { gancioClient } from './gancio.client';
|
|
import { siteSettingsService } from '../modules/settings/settings.service';
|
|
import { env } from '../config/env';
|
|
import { logger } from '../utils/logger';
|
|
|
|
// CML settings fields that trigger a Gancio sync when changed
|
|
const GANCIO_RELEVANT_FIELDS = [
|
|
'organizationName',
|
|
'organizationShortName',
|
|
'publicColorPrimary',
|
|
'publicColorBgBase',
|
|
'publicColorBgContainer',
|
|
'publicHeaderGradient',
|
|
'footerText',
|
|
'enableEvents',
|
|
// Feature flags that affect the injected nav bar
|
|
'enableInfluence',
|
|
'enableMap',
|
|
'enableMediaFeatures',
|
|
'enablePayments',
|
|
'navConfig',
|
|
] as const;
|
|
|
|
interface GancioColorPalette {
|
|
primary?: string;
|
|
error?: string;
|
|
info?: string;
|
|
success?: string;
|
|
warning?: string;
|
|
}
|
|
|
|
/**
|
|
* Generates custom CSS for Gancio to match CML's dark theme.
|
|
* Targets Vuetify component classes used in Gancio's UI.
|
|
*/
|
|
function buildCustomCss(settings: {
|
|
publicColorBgBase?: string | null;
|
|
publicColorBgContainer?: string | null;
|
|
publicColorPrimary?: string | null;
|
|
publicHeaderGradient?: string | null;
|
|
}): string {
|
|
const lines: string[] = ['/* Auto-synced from Changemaker Lite */'];
|
|
|
|
if (settings.publicColorBgBase) {
|
|
lines.push(`.v-application { background-color: ${settings.publicColorBgBase} !important; }`);
|
|
lines.push(`.v-main { background-color: ${settings.publicColorBgBase} !important; }`);
|
|
}
|
|
|
|
if (settings.publicColorBgContainer) {
|
|
lines.push(`.v-card { background-color: ${settings.publicColorBgContainer} !important; }`);
|
|
lines.push(`.v-dialog .v-card { background-color: ${settings.publicColorBgContainer} !important; }`);
|
|
}
|
|
|
|
if (settings.publicHeaderGradient) {
|
|
lines.push(`.v-app-bar { background: ${settings.publicHeaderGradient} !important; }`);
|
|
} else if (settings.publicColorPrimary) {
|
|
lines.push(`.v-app-bar { background-color: ${settings.publicColorPrimary} !important; }`);
|
|
}
|
|
|
|
// Push Gancio content down to make room for the injected CML nav bar (CSS fallback)
|
|
lines.push(`.v-application--wrap, .v-application .v-main { padding-top: 56px !important; }`);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Compact inline SVG icons matching Ant Design's outlined style.
|
|
* Stroke-based, 24x24 viewBox, rendered at 1em (14px in the nav context).
|
|
* Using fill="none" stroke="currentColor" so they inherit link color + hover transitions.
|
|
*/
|
|
const NAV_ICONS: Record<string, string> = {
|
|
Home: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
|
Campaigns: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
|
|
Map: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>',
|
|
Shifts: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
|
Events: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
|
Gallery: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>',
|
|
Donate: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
|
|
Admin: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',
|
|
Website: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
|
Docs: '<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
|
};
|
|
|
|
/**
|
|
* Generates a self-contained JS snippet that injects a CML navigation bar
|
|
* into Gancio's page. Uses Gancio's `custom_js` setting.
|
|
*
|
|
* The bar mirrors PublicLayout's header: same gradient, colors, nav links,
|
|
* inline SVG icons, and feature-flag-aware conditional rendering.
|
|
*/
|
|
interface NavConfigItem {
|
|
id: string;
|
|
label: string;
|
|
path: string;
|
|
icon: string;
|
|
enabled: boolean;
|
|
order: number;
|
|
type: 'builtin' | 'custom' | 'group';
|
|
featureFlag?: string;
|
|
external?: boolean;
|
|
children?: NavConfigItem[];
|
|
}
|
|
|
|
/** Map navConfig icon IDs to the SVG NAV_ICONS keys */
|
|
const ICON_ID_TO_KEY: Record<string, string> = {
|
|
HomeOutlined: 'Home',
|
|
SendOutlined: 'Campaigns',
|
|
EnvironmentOutlined: 'Map',
|
|
CalendarOutlined: 'Shifts',
|
|
ScheduleOutlined: 'Shifts',
|
|
BarChartOutlined: 'Events',
|
|
PlayCircleOutlined: 'Gallery',
|
|
HeartOutlined: 'Donate',
|
|
DollarOutlined: 'Donate',
|
|
ShoppingOutlined: 'Donate',
|
|
GlobalOutlined: 'Website',
|
|
BookOutlined: 'Docs',
|
|
TagOutlined: 'Events',
|
|
VideoCameraOutlined: 'Events',
|
|
FileTextOutlined: 'Docs',
|
|
TrophyOutlined: 'Home',
|
|
};
|
|
|
|
function buildCustomJs(settings: {
|
|
organizationName?: string | null;
|
|
publicHeaderGradient?: string | null;
|
|
publicColorPrimary?: string | null;
|
|
enableInfluence?: boolean | null;
|
|
enableMap?: boolean | null;
|
|
enableMediaFeatures?: boolean | null;
|
|
enablePayments?: boolean | null;
|
|
enableEvents?: boolean | null;
|
|
enableMeetingPlanner?: boolean | null;
|
|
enableTicketedEvents?: boolean | null;
|
|
enableSocial?: boolean | null;
|
|
enableMeet?: boolean | null;
|
|
enableLandingPages?: boolean | null;
|
|
navConfig?: { items: NavConfigItem[] } | null;
|
|
}, appUrl: string, homeUrl: string): string {
|
|
const orgName = settings.organizationName || 'Changemaker Lite';
|
|
const gradient = settings.publicHeaderGradient || 'linear-gradient(135deg, #005a9c 0%, #007acc 100%)';
|
|
// Strip trailing slash from URLs for consistent link construction
|
|
const baseUrl = appUrl.replace(/\/+$/, '');
|
|
const home = homeUrl.replace(/\/+$/, '');
|
|
|
|
// Build nav links array from navConfig if available, else fall back to feature flags
|
|
const links: Array<{ href: string; label: string; icon: string; active?: boolean }> = [];
|
|
|
|
if (settings.navConfig?.items) {
|
|
const featureFlagMap: Record<string, boolean | null | undefined> = {
|
|
enableInfluence: settings.enableInfluence,
|
|
enableMap: settings.enableMap,
|
|
enableMediaFeatures: settings.enableMediaFeatures,
|
|
enablePayments: settings.enablePayments,
|
|
enableEvents: settings.enableEvents,
|
|
enableMeetingPlanner: settings.enableMeetingPlanner,
|
|
enableTicketedEvents: settings.enableTicketedEvents,
|
|
enableSocial: settings.enableSocial,
|
|
enableMeet: settings.enableMeet,
|
|
enableLandingPages: settings.enableLandingPages,
|
|
};
|
|
|
|
// Flatten groups: Gancio uses a flat HTML nav bar, no dropdown support
|
|
const flatItems: NavConfigItem[] = [];
|
|
for (const item of settings.navConfig.items) {
|
|
if (item.type === 'group' && item.children) {
|
|
for (const child of item.children) flatItems.push(child);
|
|
} else {
|
|
flatItems.push(item);
|
|
}
|
|
}
|
|
|
|
const sorted = flatItems
|
|
.filter(item => item.enabled)
|
|
.filter(item => !item.featureFlag || featureFlagMap[item.featureFlag] !== false)
|
|
.sort((a, b) => a.order - b.order);
|
|
|
|
for (const item of sorted) {
|
|
// Resolve $token paths first
|
|
if (item.path === '$landing') {
|
|
links.push({ href: home, label: item.label, icon: ICON_ID_TO_KEY[item.icon] || 'Website' });
|
|
} else if (item.path === '$docs') {
|
|
links.push({ href: `${home}/docs/`, label: item.label, icon: ICON_ID_TO_KEY[item.icon] || 'Docs' });
|
|
} else if (item.id === 'events') {
|
|
// Events is the current page on Gancio, mark active
|
|
links.push({ href: '#', label: item.label, icon: ICON_ID_TO_KEY[item.icon] || 'Events', active: true });
|
|
} else if (item.external && item.id === 'home') {
|
|
links.push({ href: home, label: item.label, icon: ICON_ID_TO_KEY[item.icon] || 'Home' });
|
|
} else if (item.external) {
|
|
links.push({ href: item.path, label: item.label, icon: ICON_ID_TO_KEY[item.icon] || item.label });
|
|
} else {
|
|
links.push({ href: `${baseUrl}${item.path}`, label: item.label, icon: ICON_ID_TO_KEY[item.icon] || item.label });
|
|
}
|
|
}
|
|
} else {
|
|
// Legacy fallback: build from feature flags
|
|
links.push({ href: home, label: 'Home', icon: 'Home' });
|
|
if (settings.enableInfluence !== false) {
|
|
links.push({ href: `${baseUrl}/campaigns`, label: 'Campaigns', icon: 'Campaigns' });
|
|
}
|
|
if (settings.enableMap !== false) {
|
|
links.push({ href: `${baseUrl}/map`, label: 'Map', icon: 'Map' });
|
|
links.push({ href: `${baseUrl}/shifts`, label: 'Shifts', icon: 'Shifts' });
|
|
}
|
|
links.push({ href: '#', label: 'Events', icon: 'Events', active: true });
|
|
if (settings.enableMediaFeatures !== false) {
|
|
links.push({ href: `${baseUrl}/gallery`, label: 'Gallery', icon: 'Gallery' });
|
|
}
|
|
if (settings.enablePayments === true) {
|
|
links.push({ href: `${baseUrl}/donate`, label: 'Donate', icon: 'Donate' });
|
|
}
|
|
}
|
|
|
|
// Always add Admin link — the admin page handles its own auth guard
|
|
links.push({ href: `${baseUrl}/app`, label: 'Admin', icon: 'Admin' });
|
|
|
|
// Serialize links for embedding in JS (icon key is a string, resolved at runtime)
|
|
const linksJson = JSON.stringify(links);
|
|
|
|
// Build the icons object for the generated JS (keyed by label)
|
|
const iconsUsed = [...new Set(links.map((l) => l.icon))];
|
|
const iconsObj = iconsUsed
|
|
.map((key) => `${key}:'${NAV_ICONS[key]?.replace(/'/g, "\\'") ?? ''}'`)
|
|
.join(',');
|
|
|
|
// Safe-escaped org name for embedding in JS string literals
|
|
const orgNameEscaped = orgName.replace(/'/g, "\\'").replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
return `(function(){
|
|
if(document.getElementById('cml-nav-bar'))return;
|
|
var icons={${iconsObj}};
|
|
var bar=document.createElement('div');
|
|
bar.id='cml-nav-bar';
|
|
bar.style.cssText='position:fixed;top:0;left:0;right:0;z-index:9999;height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;background:${gradient};font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;box-sizing:border-box;';
|
|
var brand=document.createElement('a');
|
|
brand.href='${baseUrl}/campaigns';
|
|
brand.style.cssText='color:#fff;text-decoration:none;font-size:18px;font-weight:600;display:flex;align-items:center;gap:8px;white-space:nowrap;';
|
|
brand.textContent='${orgNameEscaped}';
|
|
bar.appendChild(brand);
|
|
var nav=document.createElement('div');
|
|
nav.className='cml-nav-links';
|
|
nav.style.cssText='display:flex;align-items:center;gap:16px;';
|
|
var links=${linksJson};
|
|
links.forEach(function(l){
|
|
var a=document.createElement('a');
|
|
a.href=l.href;
|
|
a.style.cssText='color:'+(l.active?'#fff':'rgba(255,255,255,0.85)')+';text-decoration:none;font-size:14px;font-weight:'+(l.active?'600':'400')+';border-bottom:'+(l.active?'2px solid #fff':'2px solid transparent')+';padding-bottom:2px;transition:color 0.2s;white-space:nowrap;display:inline-flex;align-items:center;gap:6px;';
|
|
a.innerHTML=(icons[l.icon]||'')+'<span>'+l.label+'</span>';
|
|
if(!l.active){
|
|
a.addEventListener('mouseenter',function(){this.style.color='#fff';});
|
|
a.addEventListener('mouseleave',function(){this.style.color='rgba(255,255,255,0.85)';});
|
|
}
|
|
nav.appendChild(a);
|
|
});
|
|
bar.appendChild(nav);
|
|
var mobileBack=document.createElement('a');
|
|
mobileBack.href='${baseUrl}/campaigns';
|
|
mobileBack.className='cml-nav-mobile-back';
|
|
mobileBack.style.cssText='display:none;color:#fff;text-decoration:none;font-size:14px;white-space:nowrap;';
|
|
mobileBack.textContent='\\u2190 Back to ${orgNameEscaped}';
|
|
bar.appendChild(mobileBack);
|
|
var style=document.createElement('style');
|
|
style.textContent='.v-application--wrap,.v-application .v-main{padding-top:56px!important;}@media(max-width:768px){.cml-nav-links{display:none!important;}.cml-nav-mobile-back{display:block!important;}}';
|
|
document.head.appendChild(style);
|
|
document.body.insertBefore(bar,document.body.firstChild);
|
|
})();`;
|
|
}
|
|
|
|
class GancioSettingsSyncService {
|
|
private syncInProgress = false;
|
|
|
|
/**
|
|
* Quick check: does the update payload contain any Gancio-relevant fields?
|
|
*/
|
|
hasGancioChanges(payload: Record<string, unknown>): boolean {
|
|
return GANCIO_RELEVANT_FIELDS.some((f) => f in payload);
|
|
}
|
|
|
|
/**
|
|
* Full sync: reads CML settings, reads existing Gancio settings (to preserve
|
|
* non-primary colors), then pushes all mapped settings to Gancio.
|
|
* Used at API startup.
|
|
*/
|
|
async syncAll(): Promise<void> {
|
|
if (!gancioClient.enabled) return;
|
|
|
|
if (this.syncInProgress) {
|
|
logger.debug('Gancio settings sync already in progress, skipping');
|
|
return;
|
|
}
|
|
|
|
this.syncInProgress = true;
|
|
try {
|
|
const settings = await siteSettingsService.get();
|
|
|
|
// Guard: check DB-level feature flag
|
|
if (!settings.enableEvents) {
|
|
logger.debug('Gancio sync skipped: enableEvents is false');
|
|
return;
|
|
}
|
|
|
|
// Read existing Gancio settings to preserve non-primary color values
|
|
const gancioSettings = await gancioClient.getSettings();
|
|
|
|
const promises: Promise<boolean>[] = [];
|
|
|
|
// Title & description
|
|
if (settings.organizationName) {
|
|
promises.push(gancioClient.setSetting('title', settings.organizationName));
|
|
promises.push(gancioClient.setSetting('description', `${settings.organizationName} Events`));
|
|
}
|
|
|
|
// Dark theme
|
|
promises.push(gancioClient.setSetting('theme.is_dark', true));
|
|
|
|
// Color palettes — read-then-merge to preserve error/info/success/warning
|
|
if (settings.publicColorPrimary) {
|
|
const existingDark = (gancioSettings?.dark_colors as GancioColorPalette) || {};
|
|
const existingLight = (gancioSettings?.light_colors as GancioColorPalette) || {};
|
|
|
|
promises.push(gancioClient.setSetting('dark_colors', {
|
|
...existingDark,
|
|
primary: settings.publicColorPrimary,
|
|
}));
|
|
promises.push(gancioClient.setSetting('light_colors', {
|
|
...existingLight,
|
|
primary: settings.publicColorPrimary,
|
|
}));
|
|
}
|
|
|
|
// Footer links
|
|
if (settings.footerText) {
|
|
const adminUrl = env.ADMIN_URL;
|
|
promises.push(gancioClient.setSetting('footerLinks', [
|
|
{ label: settings.footerText, href: adminUrl },
|
|
]));
|
|
}
|
|
|
|
// Custom CSS for deeper theming
|
|
const css = buildCustomCss(settings);
|
|
promises.push(gancioClient.setSetting('custom_css', css));
|
|
|
|
// Custom JS — inject CML navigation bar into Gancio
|
|
const appUrl = env.ADMIN_URL;
|
|
const homeUrl = `https://${env.DOMAIN}`;
|
|
const js = buildCustomJs(settings as any, appUrl, homeUrl);
|
|
promises.push(gancioClient.setSetting('custom_js', js));
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const succeeded = results.filter((r) => r.status === 'fulfilled' && r.value).length;
|
|
const failed = results.length - succeeded;
|
|
|
|
if (failed > 0) {
|
|
logger.warn(`Gancio settings sync: ${succeeded} succeeded, ${failed} failed`);
|
|
} else {
|
|
logger.info(`Gancio settings sync: synced ${succeeded} settings`);
|
|
}
|
|
} catch (err) {
|
|
logger.warn('Gancio settings sync failed:', err instanceof Error ? err.message : err);
|
|
} finally {
|
|
this.syncInProgress = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Partial sync: only pushes Gancio settings for fields that actually changed.
|
|
* Used from the PUT /api/settings handler.
|
|
*/
|
|
async syncChanged(changedFields: Record<string, unknown>): Promise<void> {
|
|
if (!gancioClient.enabled) return;
|
|
|
|
try {
|
|
// If enableEvents was just turned off, skip sync
|
|
if (changedFields.enableEvents === false) {
|
|
logger.debug('Gancio sync skipped: enableEvents set to false');
|
|
return;
|
|
}
|
|
|
|
// If enableEvents was just turned on, do a full sync
|
|
if (changedFields.enableEvents === true) {
|
|
await this.syncAll();
|
|
return;
|
|
}
|
|
|
|
// Check DB-level feature flag (may not be in changedFields)
|
|
const settings = await siteSettingsService.get();
|
|
if (!settings.enableEvents) {
|
|
logger.debug('Gancio sync skipped: enableEvents is false');
|
|
return;
|
|
}
|
|
|
|
const promises: Promise<boolean>[] = [];
|
|
|
|
// Organization name → title + description
|
|
if ('organizationName' in changedFields && changedFields.organizationName) {
|
|
const name = changedFields.organizationName as string;
|
|
promises.push(gancioClient.setSetting('title', name));
|
|
promises.push(gancioClient.setSetting('description', `${name} Events`));
|
|
}
|
|
|
|
// Primary color → dark_colors + light_colors (read-then-merge)
|
|
if ('publicColorPrimary' in changedFields && changedFields.publicColorPrimary) {
|
|
const gancioSettings = await gancioClient.getSettings();
|
|
const existingDark = (gancioSettings?.dark_colors as GancioColorPalette) || {};
|
|
const existingLight = (gancioSettings?.light_colors as GancioColorPalette) || {};
|
|
|
|
promises.push(gancioClient.setSetting('dark_colors', {
|
|
...existingDark,
|
|
primary: changedFields.publicColorPrimary,
|
|
}));
|
|
promises.push(gancioClient.setSetting('light_colors', {
|
|
...existingLight,
|
|
primary: changedFields.publicColorPrimary,
|
|
}));
|
|
}
|
|
|
|
// Footer text → footerLinks
|
|
if ('footerText' in changedFields) {
|
|
const adminUrl = env.ADMIN_URL;
|
|
if (changedFields.footerText) {
|
|
promises.push(gancioClient.setSetting('footerLinks', [
|
|
{ label: changedFields.footerText as string, href: adminUrl },
|
|
]));
|
|
} else {
|
|
promises.push(gancioClient.setSetting('footerLinks', []));
|
|
}
|
|
}
|
|
|
|
// Theme-related CSS fields — rebuild CSS from full settings (not just changed fields)
|
|
const cssFields = ['publicColorBgBase', 'publicColorBgContainer', 'publicHeaderGradient', 'publicColorPrimary'];
|
|
if (cssFields.some((f) => f in changedFields)) {
|
|
const css = buildCustomCss(settings);
|
|
promises.push(gancioClient.setSetting('custom_css', css));
|
|
}
|
|
|
|
// Nav bar JS — rebuild when org name, feature flags, header gradient, or navConfig change
|
|
const jsFields = [
|
|
'organizationName', 'publicHeaderGradient', 'publicColorPrimary',
|
|
'enableInfluence', 'enableMap', 'enableMediaFeatures', 'enablePayments',
|
|
'navConfig',
|
|
];
|
|
if (jsFields.some((f) => f in changedFields)) {
|
|
const appUrl = env.ADMIN_URL;
|
|
const homeUrl = `https://${env.DOMAIN}`;
|
|
const js = buildCustomJs(settings as any, appUrl, homeUrl);
|
|
promises.push(gancioClient.setSetting('custom_js', js));
|
|
}
|
|
|
|
if (promises.length === 0) return;
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const succeeded = results.filter((r) => r.status === 'fulfilled' && r.value).length;
|
|
const failed = results.length - succeeded;
|
|
|
|
if (failed > 0) {
|
|
logger.warn(`Gancio partial sync: ${succeeded}/${results.length} succeeded`);
|
|
} else {
|
|
logger.debug(`Gancio partial sync: ${succeeded} settings updated`);
|
|
}
|
|
} catch (err) {
|
|
logger.warn('Gancio partial sync failed:', err instanceof Error ? err.message : err);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const gancioSettingsSyncService = new GancioSettingsSyncService();
|