From 003517247f748061de0517ae3d940ddd3e2a0c6c Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 23 May 2026 01:22:29 +0300 Subject: [PATCH] refactor(types): migrate (window as any) statics to typed window globals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 59 sites across 19 feature modules switched from `(window as any).foo` to the typed `window.foo` form against global-types.d.ts. The 7 remaining sites use dynamic string indexing (`window[fnName]`) where a typed access is impossible — those keep the narrow cast and are documented as the legitimate exception in the typedef file's header. global-types.d.ts grows entries for `loadIntegrations`, `loadUpdateSettings`, `loadUpdateStatus`, `initUpdateSettingsPanel`, `onTestDisplaySelected`, `openSettingsModal`, `renderAboutPanel`, `switchSettingsTab`. The `applyAccentColor` signature is widened to accept the (accent, persist) call shape observed at the appearance preset call site so tsc validates the real contract. --- server/src/ledgrab/static/js/app.ts | 2 +- server/src/ledgrab/static/js/core/api.ts | 6 +++--- server/src/ledgrab/static/js/core/card-colors.ts | 2 +- .../src/ledgrab/static/js/features/appearance.ts | 2 +- .../src/ledgrab/static/js/features/automations.ts | 8 ++++---- .../static/js/features/color-strips/cards.ts | 4 ++-- .../static/js/features/color-strips/index.ts | 4 ++-- .../ledgrab/static/js/features/game-integration.ts | 6 +++--- .../static/js/features/home-assistant-sources.ts | 8 ++++---- .../ledgrab/static/js/features/http-endpoints.ts | 8 ++++---- .../src/ledgrab/static/js/features/mqtt-sources.ts | 4 ++-- .../ledgrab/static/js/features/scene-presets.ts | 4 ++-- server/src/ledgrab/static/js/features/settings.ts | 14 +++++++------- .../js/features/streams-capture-templates.ts | 4 ++-- server/src/ledgrab/static/js/features/streams.ts | 4 ++-- .../src/ledgrab/static/js/features/sync-clocks.ts | 4 ++-- server/src/ledgrab/static/js/features/update.ts | 8 ++++---- .../ledgrab/static/js/features/value-sources.ts | 4 ++-- .../ledgrab/static/js/features/weather-sources.ts | 8 ++++---- server/src/ledgrab/static/js/global-types.d.ts | 13 ++++++++++++- 20 files changed, 64 insertions(+), 53 deletions(-) diff --git a/server/src/ledgrab/static/js/app.ts b/server/src/ledgrab/static/js/app.ts index 02d4442..21bdf42 100644 --- a/server/src/ledgrab/static/js/app.ts +++ b/server/src/ledgrab/static/js/app.ts @@ -804,7 +804,7 @@ document.addEventListener('DOMContentLoaded', async () => { startConnectionMonitor(); // Expose auth state for inline scripts (after loadServerInfo sets it) - (window as any)._authRequired = authRequired; + window._authRequired = authRequired; if (typeof window.updateAuthUI === 'function') window.updateAuthUI(); // Server is unconfigured for LAN access → setup screen already shown by diff --git a/server/src/ledgrab/static/js/core/api.ts b/server/src/ledgrab/static/js/core/api.ts index 02a25fe..ec0588f 100644 --- a/server/src/ledgrab/static/js/core/api.ts +++ b/server/src/ledgrab/static/js/core/api.ts @@ -312,21 +312,21 @@ export async function loadServerInfo() { // Auth mode detection const authNeeded = data.auth_required !== false; setAuthRequired(authNeeded); - (window as any)._authRequired = authNeeded; + window._authRequired = authNeeded; // Setup-required detection (LAN client + no keys configured server-side). // When true, no API key will ever succeed — show a dedicated screen // instead of the login form. const setupNeeded = data.setup_required === true; setSetupRequired(setupNeeded); - (window as any)._setupRequired = setupNeeded; + window._setupRequired = setupNeeded; if (setupNeeded) { if (typeof window.showSetupRequiredModal === 'function') { window.showSetupRequiredModal(); } } else if (typeof window.hideSetupRequiredModal === 'function') { // Server was reconfigured — clear the setup overlay if it was up. - if ((window as any)._setupModalOpen) window.hideSetupRequiredModal(); + if (window._setupModalOpen) window.hideSetupRequiredModal(); } // Project URLs (repo, donate) diff --git a/server/src/ledgrab/static/js/core/card-colors.ts b/server/src/ledgrab/static/js/core/card-colors.ts index 718de10..fdb807b 100644 --- a/server/src/ledgrab/static/js/core/card-colors.ts +++ b/server/src/ledgrab/static/js/core/card-colors.ts @@ -177,7 +177,7 @@ function _colorPopoverHtml(pickerId: string, currentColor: string): string { function getCardColorAriaLabel(): string { // i18n is not always loaded when this module evaluates (renders // happen during early page boot). Fall back to English. - return (window as any).__t?.('common.card_color') || 'Card color'; + return window.__t?.('common.card_color') || 'Card color'; } /** diff --git a/server/src/ledgrab/static/js/features/appearance.ts b/server/src/ledgrab/static/js/features/appearance.ts index 2887aa5..c1b7c89 100644 --- a/server/src/ledgrab/static/js/features/appearance.ts +++ b/server/src/ledgrab/static/js/features/appearance.ts @@ -315,7 +315,7 @@ export function applyStylePreset(id: string): void { document.documentElement.style.setProperty('--font-heading', preset.fontHeading); // Apply accent color via existing mechanism - const applyAccent = (window as any).applyAccentColor; + const applyAccent = window.applyAccentColor; if (typeof applyAccent === 'function') { applyAccent(preset.accent, true); } else { diff --git a/server/src/ledgrab/static/js/features/automations.ts b/server/src/ledgrab/static/js/features/automations.ts index 869e50c..83da412 100644 --- a/server/src/ledgrab/static/js/features/automations.ts +++ b/server/src/ledgrab/static/js/features/automations.ts @@ -38,8 +38,8 @@ registerIconEntityType('automation', makeSimpleIconAdapter({ endpointPrefix: '/automations', reload: async () => { automationsCacheObj.invalidate(); - if (typeof (window as any).loadAutomations === 'function') { - await (window as any).loadAutomations(); + if (typeof window.loadAutomations === 'function') { + await window.loadAutomations(); } }, typeLabelKey: 'device.icon.entity.automation', @@ -653,7 +653,7 @@ export async function openAutomationEditor(automationId?: any, cloneData?: any) // Auto-name wiring _autoNameManuallyEdited = !!(automationId || cloneData); nameInput.oninput = () => { _autoNameManuallyEdited = true; }; - (window as any)._autoGenerateAutomationName = _autoGenerateAutomationName; + window._autoGenerateAutomationName = _autoGenerateAutomationName; if (!automationId && !cloneData) _autoGenerateAutomationName(); automationModal.open(); @@ -869,7 +869,7 @@ function addAutomationRuleRow(rule: any) { removeBtn.addEventListener('click', () => { _disposeHTTPPollWidgets(container); row.remove(); - const autoGen = (window as any)._autoGenerateAutomationName; + const autoGen = window._autoGenerateAutomationName; if (typeof autoGen === 'function') autoGen(); }); diff --git a/server/src/ledgrab/static/js/features/color-strips/cards.ts b/server/src/ledgrab/static/js/features/color-strips/cards.ts index ba6d0e8..4c4332e 100644 --- a/server/src/ledgrab/static/js/features/color-strips/cards.ts +++ b/server/src/ledgrab/static/js/features/color-strips/cards.ts @@ -31,8 +31,8 @@ registerIconEntityType('color_strip_source', makeSimpleIconAdapter { colorStripSourcesCache.invalidate(); - if (typeof (window as any).loadPictureSources === 'function') { - await (window as any).loadPictureSources(); + if (typeof window.loadPictureSources === 'function') { + await window.loadPictureSources(); } }, typeLabelKey: 'device.icon.entity.color_strip_source', diff --git a/server/src/ledgrab/static/js/features/color-strips/index.ts b/server/src/ledgrab/static/js/features/color-strips/index.ts index 07d68d7..f374c91 100644 --- a/server/src/ledgrab/static/js/features/color-strips/index.ts +++ b/server/src/ledgrab/static/js/features/color-strips/index.ts @@ -263,7 +263,7 @@ function _openKCRegionEditor(): void { }); } -(window as any)._openKCRegionEditor = _openKCRegionEditor; +window._openKCRegionEditor = _openKCRegionEditor; async function configureKCRegions(sourceId: string): Promise { try { @@ -296,7 +296,7 @@ async function configureKCRegions(sourceId: string): Promise { showToast(e.message, 'error'); } } -(window as any).configureKCRegions = configureKCRegions; +window.configureKCRegions = configureKCRegions; // ══════════════════════════════════════════════════════════════════ // Type selector diff --git a/server/src/ledgrab/static/js/features/game-integration.ts b/server/src/ledgrab/static/js/features/game-integration.ts index e8f4970..111fe4f 100644 --- a/server/src/ledgrab/static/js/features/game-integration.ts +++ b/server/src/ledgrab/static/js/features/game-integration.ts @@ -32,8 +32,8 @@ registerIconEntityType('game_integration', makeSimpleIconAdapter { gameIntegrationsCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.game_integration', @@ -799,5 +799,5 @@ export async function loadGameIntegrations() { gameAdaptersCache.fetch(), ]); // Integrations.ts handles rendering via loadIntegrations - if ((window as any).loadIntegrations) (window as any).loadIntegrations(); + if (window.loadIntegrations) window.loadIntegrations(); } diff --git a/server/src/ledgrab/static/js/features/home-assistant-sources.ts b/server/src/ledgrab/static/js/features/home-assistant-sources.ts index ae96bff..a05fa36 100644 --- a/server/src/ledgrab/static/js/features/home-assistant-sources.ts +++ b/server/src/ledgrab/static/js/features/home-assistant-sources.ts @@ -23,8 +23,8 @@ registerIconEntityType('ha_source', makeSimpleIconAdapter({ cache: haSourcesCache, endpointPrefix: '/home-assistant/sources', reload: async () => { - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.ha_source', @@ -188,7 +188,7 @@ export async function saveHASource(): Promise { showToast(t(id ? 'ha_source.updated' : 'ha_source.created'), 'success'); haSourceModal.forceClose(); haSourcesCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') await window.loadIntegrations(); } catch (e: any) { if (e.isAuth) return; haSourceModal.showError(e.message); @@ -234,7 +234,7 @@ export async function deleteHASource(sourceId: string): Promise { } showToast(t('ha_source.deleted'), 'success'); haSourcesCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') await window.loadIntegrations(); } catch (e: any) { if (e.isAuth) return; showToast(e.message, 'error'); diff --git a/server/src/ledgrab/static/js/features/http-endpoints.ts b/server/src/ledgrab/static/js/features/http-endpoints.ts index f452d7b..c960930 100644 --- a/server/src/ledgrab/static/js/features/http-endpoints.ts +++ b/server/src/ledgrab/static/js/features/http-endpoints.ts @@ -34,8 +34,8 @@ registerIconEntityType('http_endpoint', makeSimpleIconAdapter({ cache: httpEndpointsCache, endpointPrefix: '/http/endpoints', reload: async () => { - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.http_endpoint', @@ -329,7 +329,7 @@ export async function saveHTTPEndpoint(): Promise { showToast(t(id ? 'http_endpoint.updated' : 'http_endpoint.created'), 'success'); httpEndpointModal.forceClose(); httpEndpointsCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') await window.loadIntegrations(); } catch (e: any) { if (e.isAuth) return; httpEndpointModal.showError(e.message); @@ -377,7 +377,7 @@ export async function deleteHTTPEndpoint(endpointId: string): Promise { } showToast(t('http_endpoint.deleted'), 'success'); httpEndpointsCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') await window.loadIntegrations(); } catch (e: any) { if (e.isAuth) return; showToast(e.message, 'error'); diff --git a/server/src/ledgrab/static/js/features/mqtt-sources.ts b/server/src/ledgrab/static/js/features/mqtt-sources.ts index cb503f4..c766d98 100644 --- a/server/src/ledgrab/static/js/features/mqtt-sources.ts +++ b/server/src/ledgrab/static/js/features/mqtt-sources.ts @@ -20,8 +20,8 @@ registerIconEntityType('mqtt_source', makeSimpleIconAdapter({ cache: mqttSourcesCache, endpointPrefix: '/mqtt/sources', reload: async () => { - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.mqtt_source', diff --git a/server/src/ledgrab/static/js/features/scene-presets.ts b/server/src/ledgrab/static/js/features/scene-presets.ts index 0418763..bda9c9d 100644 --- a/server/src/ledgrab/static/js/features/scene-presets.ts +++ b/server/src/ledgrab/static/js/features/scene-presets.ts @@ -28,8 +28,8 @@ registerIconEntityType('scene_preset', makeSimpleIconAdapter({ endpointPrefix: '/scene-presets', reload: async () => { scenePresetsCache.invalidate(); - if (typeof (window as any).loadAutomations === 'function') { - await (window as any).loadAutomations(); + if (typeof window.loadAutomations === 'function') { + await window.loadAutomations(); } }, typeLabelKey: 'device.icon.entity.scene_preset', diff --git a/server/src/ledgrab/static/js/features/settings.ts b/server/src/ledgrab/static/js/features/settings.ts index 904beb5..b62670b 100644 --- a/server/src/ledgrab/static/js/features/settings.ts +++ b/server/src/ledgrab/static/js/features/settings.ts @@ -134,13 +134,13 @@ export function switchSettingsTab(tabId: string): void { window.renderAppearanceTab(); } // Lazy-load update settings - if (tabId === 'updates' && typeof (window as any).loadUpdateSettings === 'function') { - (window as any).initUpdateSettingsPanel(); - (window as any).loadUpdateSettings(); + if (tabId === 'updates' && typeof window.loadUpdateSettings === 'function') { + window.initUpdateSettingsPanel?.(); + window.loadUpdateSettings(); } // Lazy-render the about panel content - if (tabId === 'about' && typeof (window as any).renderAboutPanel === 'function') { - (window as any).renderAboutPanel(); + if (tabId === 'about' && typeof window.renderAboutPanel === 'function') { + window.renderAboutPanel(); } // Lazy-render the notifications panel (build IconSelects + load prefs) if (tabId === 'notifications') { @@ -530,8 +530,8 @@ export function openSettingsModal(): void { // Refresh the update status so the rail badge ("update available" pill // on the Updates tab) is current when the modal opens — it would // otherwise reflect whatever state the app loaded with. - if (typeof (window as any).loadUpdateStatus === 'function') { - (window as any).loadUpdateStatus(); + if (typeof window.loadUpdateStatus === 'function') { + window.loadUpdateStatus(); } } diff --git a/server/src/ledgrab/static/js/features/streams-capture-templates.ts b/server/src/ledgrab/static/js/features/streams-capture-templates.ts index c5f8283..35e77f9 100644 --- a/server/src/ledgrab/static/js/features/streams-capture-templates.ts +++ b/server/src/ledgrab/static/js/features/streams-capture-templates.ts @@ -399,7 +399,7 @@ export function openTestDisplayPicker() { const engineType = currentTestingTemplate?.engine_type || null; const pickerEngineType = _engineHasOwnDisplays(engineType) ? engineType : null; const currentValue = (document.getElementById('test-template-display') as HTMLInputElement).value; - openDisplayPicker((window as any).onTestDisplaySelected, currentValue, pickerEngineType); + openDisplayPicker(window.onTestDisplaySelected, currentValue, pickerEngineType); } async function loadDisplaysForTest() { @@ -436,7 +436,7 @@ async function loadDisplaysForTest() { if (selectedIndex !== null && _cachedDisplays) { const display = _cachedDisplays.find(d => d.index === selectedIndex); - (window as any).onTestDisplaySelected(selectedIndex, display); + window.onTestDisplaySelected(selectedIndex, display); } } catch (error) { console.error('Error loading displays:', error); diff --git a/server/src/ledgrab/static/js/features/streams.ts b/server/src/ledgrab/static/js/features/streams.ts index 73cb0ae..f6ddf30 100644 --- a/server/src/ledgrab/static/js/features/streams.ts +++ b/server/src/ledgrab/static/js/features/streams.ts @@ -80,8 +80,8 @@ import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts' // ── Icon-picker adapter registrations for streams-tab card types ── const _reloadStreams = async () => { - if (typeof (window as any).loadPictureSources === 'function') { - await (window as any).loadPictureSources(); + if (typeof window.loadPictureSources === 'function') { + await window.loadPictureSources(); } }; diff --git a/server/src/ledgrab/static/js/features/sync-clocks.ts b/server/src/ledgrab/static/js/features/sync-clocks.ts index 50e2212..ccff4a8 100644 --- a/server/src/ledgrab/static/js/features/sync-clocks.ts +++ b/server/src/ledgrab/static/js/features/sync-clocks.ts @@ -21,8 +21,8 @@ registerIconEntityType('sync_clock', makeSimpleIconAdapter({ endpointPrefix: '/sync-clocks', reload: async () => { syncClocksCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.sync_clock', diff --git a/server/src/ledgrab/static/js/features/update.ts b/server/src/ledgrab/static/js/features/update.ts index 03bfd24..39105c1 100644 --- a/server/src/ledgrab/static/js/features/update.ts +++ b/server/src/ledgrab/static/js/features/update.ts @@ -64,12 +64,12 @@ function _setVersionBadgeUpdate(hasUpdate: boolean): void { } export function switchSettingsTabToUpdate(): void { - if (typeof (window as any).openSettingsModal === 'function') { - (window as any).openSettingsModal(); + if (typeof window.openSettingsModal === 'function') { + window.openSettingsModal(); } setTimeout(() => { - if (typeof (window as any).switchSettingsTab === 'function') { - (window as any).switchSettingsTab('updates'); + if (typeof window.switchSettingsTab === 'function') { + window.switchSettingsTab('updates'); } }, 50); } diff --git a/server/src/ledgrab/static/js/features/value-sources.ts b/server/src/ledgrab/static/js/features/value-sources.ts index 63457d3..0291701 100644 --- a/server/src/ledgrab/static/js/features/value-sources.ts +++ b/server/src/ledgrab/static/js/features/value-sources.ts @@ -42,8 +42,8 @@ registerIconEntityType('value_source', makeSimpleIconAdapter({ endpointPrefix: '/value-sources', reload: async () => { valueSourcesCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.value_source', diff --git a/server/src/ledgrab/static/js/features/weather-sources.ts b/server/src/ledgrab/static/js/features/weather-sources.ts index 4c1518b..b7df921 100644 --- a/server/src/ledgrab/static/js/features/weather-sources.ts +++ b/server/src/ledgrab/static/js/features/weather-sources.ts @@ -21,8 +21,8 @@ registerIconEntityType('weather_source', makeSimpleIconAdapter({ cache: weatherSourcesCache, endpointPrefix: '/weather-sources', reload: async () => { - if (typeof (window as any).loadIntegrations === 'function') { - await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') { + await window.loadIntegrations(); } }, typeLabelKey: 'device.icon.entity.weather_source', @@ -180,7 +180,7 @@ export async function saveWeatherSource(): Promise { showToast(t(id ? 'weather_source.updated' : 'weather_source.created'), 'success'); weatherSourceModal.forceClose(); weatherSourcesCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') await window.loadIntegrations(); } catch (e: any) { if (e.isAuth) return; weatherSourceModal.showError(e.message); @@ -226,7 +226,7 @@ export async function deleteWeatherSource(sourceId: string): Promise { } showToast(t('weather_source.deleted'), 'success'); weatherSourcesCache.invalidate(); - if (typeof (window as any).loadIntegrations === 'function') await (window as any).loadIntegrations(); + if (typeof window.loadIntegrations === 'function') await window.loadIntegrations(); } catch (e: any) { if (e.isAuth) return; showToast(e.message, 'error'); diff --git a/server/src/ledgrab/static/js/global-types.d.ts b/server/src/ledgrab/static/js/global-types.d.ts index 4222ae4..19b355a 100644 --- a/server/src/ledgrab/static/js/global-types.d.ts +++ b/server/src/ledgrab/static/js/global-types.d.ts @@ -27,7 +27,7 @@ declare global { __t?: (key: string) => string; // UI helpers exposed for inline onclick handlers in templates. - applyAccentColor?: () => void; + applyAccentColor?: (accent?: string, persist?: boolean) => void; hideSetupRequiredModal?: () => void; configureKCRegions?: (sourceId: string) => void; removeZ2MLightMapping?: (btn: HTMLElement) => void; @@ -36,8 +36,19 @@ declare global { // doesn't want a hard module import (to avoid cycles). loadAutomations?: () => Promise | void; loadPictureSources?: () => Promise | void; + loadIntegrations?: () => Promise | void; + loadUpdateSettings?: () => Promise | void; + loadUpdateStatus?: () => Promise | void; showPatternTemplateEditor?: (...args: unknown[]) => unknown; + // Settings + about panel — bound by app boot and called from + // template handlers / nav buttons. + initUpdateSettingsPanel?: () => void; + onTestDisplaySelected?: (value: string) => void; + openSettingsModal?: (...args: unknown[]) => unknown; + renderAboutPanel?: () => void; + switchSettingsTab?: (tabId: string) => void; + // Internal helpers attached by features for inline-call reuse. _autoGenerateAutomationName?: () => void; _openKCRegionEditor?: (sourceId: string) => void;