From 515052fbdadaa0c9b85e3dab6572bda0f3690f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=BCckner?= Date: Wed, 10 Jun 2026 14:43:31 +0200 Subject: [PATCH] refactor: replace CADDY_MANAGER with DEPLOY_ENV for instance-role awareness DEPLOY_ENV=production now marks the primary instance globally - used for Caddy ownership, the Dev/Prod header badge, and Caddy UI gating. Removes build-time VITE_DEPLOY_ENV/import.meta.env.DEV from the header in favour of the runtime API response (isProduction field in /api/auth/config). --- deploy/proxmox-ghostgrid.sh | 4 ++-- server.ts | 20 ++++++++++---------- src/App.tsx | 4 +++- src/components/Header.tsx | 6 ++++-- src/components/Settings.tsx | 30 +++++++++++++++--------------- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/deploy/proxmox-ghostgrid.sh b/deploy/proxmox-ghostgrid.sh index d6bd8fe..73a7ac0 100644 --- a/deploy/proxmox-ghostgrid.sh +++ b/deploy/proxmox-ghostgrid.sh @@ -190,8 +190,8 @@ msg_info "Creating .env file for each instance" for d in "${APP_DIR}" "${DEV_DIR}"; do SECRET="$(openssl rand -hex 32)" run "printf 'JWT_SECRET=\"%s\"\n' '${SECRET}' > ${d}/.env && chown ghostgrid:ghostgrid ${d}/.env && chmod 600 ${d}/.env" - # Only the production instance owns the shared Caddy (one Caddy per container). - [[ "$d" == "${APP_DIR}" ]] && run "printf 'CADDY_MANAGER=true\n' >> ${d}/.env" + # Only the production instance owns Caddy and shows "Production" in the UI. + [[ "$d" == "${APP_DIR}" ]] && run "printf 'DEPLOY_ENV=production\n' >> ${d}/.env" done msg_ok ".env files created (main + dev)" diff --git a/server.ts b/server.ts index 4ef13db..e54b966 100644 --- a/server.ts +++ b/server.ts @@ -15,10 +15,10 @@ const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toStrin const JWT_SECRET = process.env.JWT_SECRET || 'ghostgrid-dev-secret-change-in-production'; const JWT_EXPIRY = '24h'; -// One Caddy serves the whole container; only the instance with CADDY_MANAGER=true -// owns it (pushes config, seeds routes, accepts route edits). The other instance -// must never push — POST /load replaces the entire config and would clobber it. -const IS_CADDY_MANAGER = process.env.CADDY_MANAGER === 'true'; +// DEPLOY_ENV=production marks the primary instance: it owns Caddy (pushes config, +// seeds routes, accepts route edits) and shows "Production" in the UI. The dev +// instance must never push to Caddy — POST /load replaces the entire config. +const IS_PRODUCTION = process.env.DEPLOY_ENV === 'production'; interface JwtPayload { userId: string; @@ -153,7 +153,7 @@ function importCaddyfileRoutes(userId?: string): void { } async function pushCaddyConfig(): Promise { - if (!IS_CADDY_MANAGER) return; + if (!IS_PRODUCTION) return; if (getSetting('caddy_enabled') !== 'true') return; const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019'; const body = buildCaddyfile(); @@ -180,7 +180,7 @@ async function startServer() { console.log('[Init] Default admin user created — login: admin@ghostgrid.local / admin'); } - if (IS_CADDY_MANAGER && getSetting('caddy_enabled') === 'true' && getCaddyRoutes().length === 0) { + if (IS_PRODUCTION && getSetting('caddy_enabled') === 'true' && getCaddyRoutes().length === 0) { importCaddyfileRoutes(); } @@ -277,7 +277,7 @@ async function startServer() { effectiveRedirectUri, checkmkEnabled: getSetting('checkmk_enabled') === 'true', checkmkBaseUrl: cmkApiUrl.replace(/\/api\/.*$/, ''), - caddyManaged: IS_CADDY_MANAGER, + isProduction: IS_PRODUCTION, }); }); @@ -1205,7 +1205,7 @@ async function startServer() { app.post('/api/caddy/routes', requireAuth, async (req, res) => { try { - if (!IS_CADDY_MANAGER) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); + if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); const { hostname, upstream, tls, compress, redirectPath } = req.body as { hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: string; }; @@ -1225,7 +1225,7 @@ async function startServer() { app.put('/api/caddy/routes/:id', requireAuth, async (req, res) => { try { - if (!IS_CADDY_MANAGER) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); + if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); const id = Number(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid route id.' }); const { hostname, upstream, tls, compress, redirectPath } = req.body as { @@ -1245,7 +1245,7 @@ async function startServer() { app.delete('/api/caddy/routes/:id', requireAuth, (req, res) => { try { - if (!IS_CADDY_MANAGER) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); + if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); const id = Number(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid route id.' }); const existing = getCaddyRouteById(id); diff --git a/src/App.tsx b/src/App.tsx index 0c520ba..1b1df52 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -55,6 +55,7 @@ export default function App() { const [inventoryHighlightDevice, setInventoryHighlightDevice] = useState(null); const [checkmkEnabled, setCheckmkEnabled] = useState(false); const [checkmkBaseUrl, setCheckmkBaseUrl] = useState(''); + const [isProduction, setIsProduction] = useState(false); useEffect(() => { const root = document.documentElement; @@ -143,7 +144,7 @@ export default function App() { if (bookingsRes.ok) setBookings(await bookingsRes.json()); if (logsRes.ok) setLogs(await logsRes.json()); if (linksRes.ok) setLinks(await linksRes.json()); - if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); setCheckmkBaseUrl(cfg.checkmkBaseUrl || ''); } + if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); setCheckmkBaseUrl(cfg.checkmkBaseUrl || ''); setIsProduction(!!cfg.isProduction); } } catch (err) { console.error('[App] Failed to load data:', err); } finally { @@ -484,6 +485,7 @@ export default function App() { theme={theme} onThemeToggle={() => setTheme(t => t === 'dark' ? 'light' : 'dark')} onLogout={handleLogout} + isProduction={isProduction} />
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4a94187..a22abb1 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -11,6 +11,7 @@ interface HeaderProps { theme: 'dark' | 'light'; onThemeToggle: () => void; onLogout: () => void; + isProduction: boolean; } export default function Header({ @@ -22,6 +23,7 @@ export default function Header({ theme, onThemeToggle, onLogout, + isProduction, }: HeaderProps) { const [showMailInbox, setShowMailInbox] = useState(false); const [showBellDropdown, setShowBellDropdown] = useState(false); @@ -63,8 +65,8 @@ export default function Header({ {/* System Indicator */}
- - System: {(import.meta.env.VITE_DEPLOY_ENV === 'dev' || import.meta.env.DEV) ? 'Development' : 'Production'} + + System: {isProduction ? 'Production' : 'Development'}
{/* Mail Inbox */} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 13bb65d..2fbf0d1 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -193,7 +193,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { const [caddyEnabled, setCaddyEnabled] = useState(false); const [caddyManaged, setCaddyManaged] = useState(true); - const [caddyAdminUrl, setCaddyAdminUrl] = useState('http://localhost:2019'); + const [caddyAdminUrl, setCaddyAdminUrl] = useState('http://127.0.0.1:2019'); const [caddyStatus, setCaddyStatus] = useState<'unknown' | 'available' | 'unavailable'>('unknown'); const [caddyRoutes, setCaddyRoutes] = useState([]); const [addingRoute, setAddingRoute] = useState(false); @@ -225,7 +225,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { .then(r => r.json()) .then(d => { if (d.effectiveRedirectUri) setEffectiveRedirectUri(d.effectiveRedirectUri); - setCaddyManaged(d.caddyManaged !== false); + setCaddyManaged(d.isProduction !== false); }) .catch(() => {}); }, []); @@ -265,7 +265,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { setSemaphoreApiToken(''); setSemaphoreProjectId(data.semaphore_project_id || ''); setCaddyEnabled(data.caddy_enabled === 'true'); - setCaddyAdminUrl(data.caddy_admin_url || 'http://localhost:2019'); + setCaddyAdminUrl(data.caddy_admin_url || 'http://127.0.0.1:2019'); } catch { setError('Network error loading settings.'); } finally { @@ -350,7 +350,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { return; } const templates = await res.json() as any[]; - setSemaphoreTestResult(`Connected — ${templates.length} task template${templates.length !== 1 ? 's' : ''} found.`); + setSemaphoreTestResult(`Connected - ${templates.length} task template${templates.length !== 1 ? 's' : ''} found.`); } catch { setSemaphoreTestResult('Error: Network error connecting to Semaphore.'); } finally { @@ -371,7 +371,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { const res = await authFetch('/api/caddy/routes'); if (res.ok) setCaddyRoutes(await res.json()); } catch {} - // Status check runs separately — purely informational, never blocks the list + // Status check runs separately - purely informational, never blocks the list authFetch('/api/caddy/status') .then(res => res.ok ? res.json() : null) .then(s => setCaddyStatus(s?.available ? 'available' : 'unavailable')) @@ -570,7 +570,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
)} - {/* Section tabs — switch between Integrations and System to keep the page light */} + {/* Section tabs - switch between Integrations and System to keep the page light */}
{([ { id: 'integrations', label: 'Integrations', icon: }, @@ -748,7 +748,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { />
- + - - } /> + + } /> {/* Route list */} {caddyEnabled && (
- Prefix the upstream with https:// for TLS backends (e.g. Semaphore) — the certificate is not verified. + Prefix the upstream with https:// for TLS backends - the certificate is not verified. {caddyStatus === 'unavailable' && (

- Caddy Admin API not reachable — routes will be applied when Caddy starts. + Caddy Admin API not reachable - routes will be applied when Caddy starts.

)} @@ -1003,7 +1003,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
- +
) : (
@@ -1071,7 +1071,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { {addingRoute ? 'Adding…' : 'Add'}
- +
)} @@ -1095,7 +1095,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {

- {dbInfo ? formatBytes(dbInfo.sizeBytes) : '—'} + {dbInfo ? formatBytes(dbInfo.sizeBytes) : '-'}

{dbInfo ? new Date(dbInfo.lastModified).toLocaleDateString() : 'Loading…'} @@ -1172,7 +1172,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {

- Import overwrites the entire database — this cannot be undone. + Import overwrites the entire database - this cannot be undone.