Files
wled-screen-controller-mixed/server/src/wled_controller/static/js/app.ts
alexei.dolgolyov cb9289f01f
Some checks failed
Lint & Test / test (push) Has been cancelled
feat: HA light output targets — cast LED colors to Home Assistant lights
New output target type `ha_light` that sends averaged LED colors to HA
light entities via WebSocket service calls (light.turn_on/turn_off):

Backend:
- HARuntime.call_service(): fire-and-forget WS service calls
- HALightOutputTarget: data model with light mappings, update rate, transition
- HALightTargetProcessor: processing loop with delta detection, rate limiting
- ProcessorManager.add_ha_light_target(): registration
- API schemas/routes updated for ha_light target type

Frontend:
- HA Light Targets section in Targets tab tree nav
- Modal editor: HA source picker, CSS source picker, light entity mappings
- Target cards with start/stop/clone/edit actions
- i18n keys for all new UI strings
2026-03-28 00:08:49 +03:00

744 lines
23 KiB
TypeScript

/**
* Entry point — imports all modules, registers globals, initializes app.
*/
// Layer 0: state
import { apiKey, setApiKey, authRequired, refreshInterval } from './core/state.ts';
import { Modal } from './core/modal.ts';
import { queryEl } from './core/dom-utils.ts';
// Layer 1: api, i18n
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor, serverRepoUrl, serverDonateUrl } from './core/api.ts';
import { t, initLocale, changeLocale } from './core/i18n.ts';
// Layer 1.5: visual effects
import { initCardGlare } from './core/card-glare.ts';
import { initBgAnim, updateBgAnimAccent, updateBgAnimTheme } from './core/bg-anim.ts';
import { initBgShaders } from './core/bg-shaders.ts';
import { initTabIndicator, updateTabIndicator } from './core/tab-indicator.ts';
// Layer 2: ui
import {
toggleHint, lockBody, unlockBody, closeLightbox,
showToast, showUndoToast, showConfirm, closeConfirmModal,
openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner,
setFieldError, clearFieldError, setupBlurValidation,
} from './core/ui.ts';
// Layer 3: displays, tutorials
import {
openDisplayPicker, closeDisplayPicker, selectDisplay, formatDisplayLabel,
} from './features/displays.ts';
import {
startCalibrationTutorial, startDeviceTutorial, startGettingStartedTutorial,
startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startAutomationsTutorial,
closeTutorial, tutorialNext, tutorialPrev,
} from './features/tutorials.ts';
// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, automations
import {
showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
turnOffDevice, pingDevice, removeDevice, loadDevices,
updateSettingsBaudFpsHint, copyWsUrl,
} from './features/devices.ts';
import {
loadDashboard, stopUptimeTimer,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.ts';
import { startEventsWS, stopEventsWS } from './core/events-ws.ts';
import { startEntityEventListeners } from './core/entity-events.ts';
import {
startPerfPolling, stopPerfPolling, setPerfMode,
} from './features/perf-charts.ts';
import {
loadPictureSources, switchStreamTab,
showAddTemplateModal, editTemplate, closeTemplateModal, saveTemplate, deleteTemplate,
showTestTemplateModal, closeTestTemplateModal, onEngineChange, runTemplateTest,
showAddAudioTemplateModal, editAudioTemplate, closeAudioTemplateModal, saveAudioTemplate, deleteAudioTemplate,
cloneAudioTemplate, onAudioEngineChange,
showTestAudioTemplateModal, closeTestAudioTemplateModal, startAudioTemplateTest,
showAddStreamModal, editStream, closeStreamModal, saveStream, deleteStream,
onStreamTypeChange, onStreamDisplaySelected, onTestDisplaySelected,
showTestStreamModal, closeTestStreamModal, updateStreamTestDuration, runStreamTest,
showTestPPTemplateModal, closeTestPPTemplateModal, updatePPTestDuration, runPPTemplateTest,
showAddPPTemplateModal, editPPTemplate, closePPTemplateModal, savePPTemplate, deletePPTemplate,
addFilterFromSelect, toggleFilterExpand, removeFilter, moveFilter, updateFilterOption,
renderModalFilterList, updateCaptureDuration,
cloneStream, cloneCaptureTemplate, clonePPTemplate,
showAddCSPTModal, editCSPT, closeCSPTModal, saveCSPT, deleteCSPT, cloneCSPT,
csptAddFilterFromSelect, csptToggleFilterExpand, csptRemoveFilter, csptUpdateFilterOption,
renderCSPTModalFilterList,
} from './features/streams.ts';
import {
createKCTargetCard, testKCTarget,
showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor,
deleteKCTarget, disconnectAllKCWebSockets,
updateKCBrightnessLabel, saveKCBrightness,
cloneKCTarget,
} from './features/kc-targets.ts';
import {
createPatternTemplateCard,
showPatternTemplateEditor, closePatternTemplateModal, forceClosePatternTemplateModal,
savePatternTemplate, deletePatternTemplate,
renderPatternRectList, selectPatternRect, updatePatternRect,
addPatternRect, deleteSelectedPatternRect, removePatternRect,
capturePatternBackground,
clonePatternTemplate,
} from './features/pattern-templates.ts';
import {
loadAutomations, switchAutomationTab, openAutomationEditor, closeAutomationEditorModal,
saveAutomationEditor, addAutomationCondition,
toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl,
} from './features/automations.ts';
import {
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
activateScenePreset, cloneScenePreset, deleteScenePreset,
addSceneTarget,
} from './features/scene-presets.ts';
// Layer 5: device-discovery, targets
import {
onDeviceTypeChanged, updateBaudFpsHint, onSerialPortFocus,
showAddDevice, closeAddDeviceModal, scanForDevices, handleAddDevice,
cloneDevice,
} from './features/device-discovery.ts';
import {
loadTargetsTab, switchTargetSubTab,
showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
startTargetProcessing, stopTargetProcessing,
stopAllLedTargets, stopAllKCTargets,
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview,
disconnectAllLedPreviewWS,
} from './features/targets.ts';
// Layer 5: color-strip sources
import {
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
onCSSTypeChange, onEffectTypeChange, onEffectPaletteChange, onAnimationTypeChange, onCSSClockChange, onDaylightRealTimeChange,
colorCycleAddColor, colorCycleRemoveColor,
compositeAddLayer, compositeRemoveLayer,
mappedAddZone, mappedRemoveZone,
onAudioVizChange,
onGradientPresetChange,
promptAndSaveGradientPreset,
deleteAndRefreshGradientPreset,
showGradientModal,
closeGradientEditor,
saveGradientEntity,
cloneGradient,
editGradient,
deleteGradient,
cloneColorStrip,
toggleCSSOverlay,
previewCSSFromEditor,
copyEndpointUrl,
onNotificationFilterModeChange,
notificationAddAppOverride, notificationRemoveAppOverride,
testNotification,
showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer,
} from './features/color-strips.ts';
// Layer 5: audio sources
import {
showAudioSourceModal, closeAudioSourceModal, saveAudioSource,
editAudioSource, cloneAudioSource, deleteAudioSource,
testAudioSource, closeTestAudioSourceModal,
refreshAudioDevices,
} from './features/audio-sources.ts';
// Layer 5: value sources
import {
showValueSourceModal, closeValueSourceModal, saveValueSource,
editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource, closeTestValueSourceModal,
} from './features/value-sources.ts';
// Layer 5: calibration
import {
showCalibration, closeCalibrationModal, forceCloseCalibrationModal, saveCalibration,
updateOffsetSkipLock, updateCalibrationPreview,
setStartPosition, toggleEdgeInputs, toggleDirection, toggleTestEdge,
showCSSCalibration, toggleCalibrationOverlay,
} from './features/calibration.ts';
import {
showAdvancedCalibration, closeAdvancedCalibration, saveAdvancedCalibration,
addCalibrationLine, removeCalibrationLine, selectCalibrationLine, moveCalibrationLine,
updateCalibrationLine, resetCalibrationView,
} from './features/advanced-calibration.ts';
// Layer 5.5: graph editor
import {
loadGraphEditor,
toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo,
graphFitAll, graphZoomIn, graphZoomOut, graphRelayout,
graphToggleFullscreen, graphAddEntity,
} from './features/graph-editor.ts';
// Layer 6: tabs, navigation, command palette, settings
import { switchTab, initTabs, startAutoRefresh, handlePopState } from './features/tabs.ts';
import { navigateToCard } from './core/navigation.ts';
import { openCommandPalette, closeCommandPalette, initCommandPalette } from './core/command-palette.ts';
import {
applyStylePreset, applyBgEffect, renderAppearanceTab, initAppearance,
} from './features/appearance.ts';
import {
openSettingsModal, closeSettingsModal, switchSettingsTab,
downloadBackup, handleRestoreFileSelected,
saveAutoBackupSettings, triggerBackupNow, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
restartServer, saveMqttSettings,
loadApiKeysList,
connectLogViewer, disconnectLogViewer, clearLogViewer, applyLogFilter,
openLogOverlay, closeLogOverlay,
loadLogLevel, setLogLevel,
saveExternalUrl, getBaseOrigin, loadExternalUrl,
} from './features/settings.ts';
import {
loadUpdateStatus, initUpdateListener, checkForUpdates,
loadUpdateSettings, saveUpdateSettings, dismissUpdate,
initUpdateSettingsPanel, applyUpdate,
openReleaseNotes, closeReleaseNotes,
} from './features/update.ts';
import {
initDonationBanner, dismissDonation, snoozeDonation, renderAboutPanel, setProjectUrls,
} from './features/donation.ts';
// ─── Register all HTML onclick / onchange / onfocus globals ───
Object.assign(window, {
// core / state (for inline script)
setApiKey,
// visual effects (called from inline <script>)
_updateBgAnimAccent: updateBgAnimAccent,
_updateBgAnimTheme: updateBgAnimTheme,
_updateTabIndicator: updateTabIndicator,
// core / ui
toggleHint,
lockBody,
unlockBody,
closeLightbox,
showToast,
showUndoToast,
showConfirm,
closeConfirmModal,
openFullImageLightbox,
showOverlaySpinner,
hideOverlaySpinner,
setFieldError,
clearFieldError,
setupBlurValidation,
// core / api + i18n
t,
configureApiKey,
loadServerInfo,
loadDisplays,
changeLocale,
// displays
openDisplayPicker,
closeDisplayPicker,
selectDisplay,
formatDisplayLabel,
// tutorials
startCalibrationTutorial,
startDeviceTutorial,
startGettingStartedTutorial,
startDashboardTutorial,
startTargetsTutorial,
startSourcesTutorial,
startAutomationsTutorial,
closeTutorial,
tutorialNext,
tutorialPrev,
// devices
showSettings,
closeDeviceSettingsModal,
forceCloseDeviceSettingsModal,
saveDeviceSettings,
updateBrightnessLabel,
saveCardBrightness,
turnOffDevice,
pingDevice,
removeDevice,
loadDevices,
updateSettingsBaudFpsHint,
copyWsUrl,
cloneDevice,
// dashboard
loadDashboard,
dashboardToggleAutomation,
dashboardStartTarget,
dashboardStopTarget,
dashboardStopAll,
dashboardPauseClock,
dashboardResumeClock,
dashboardResetClock,
toggleDashboardSection,
changeDashboardPollInterval,
stopUptimeTimer,
startPerfPolling,
stopPerfPolling,
setPerfMode,
// streams / capture templates / PP templates
loadPictureSources,
switchStreamTab,
showAddTemplateModal,
editTemplate,
closeTemplateModal,
saveTemplate,
deleteTemplate,
showTestTemplateModal,
closeTestTemplateModal,
onEngineChange,
runTemplateTest,
updateCaptureDuration,
showAddStreamModal,
editStream,
closeStreamModal,
saveStream,
deleteStream,
onStreamTypeChange,
onStreamDisplaySelected,
onTestDisplaySelected,
showTestStreamModal,
closeTestStreamModal,
updateStreamTestDuration,
runStreamTest,
showTestPPTemplateModal,
closeTestPPTemplateModal,
updatePPTestDuration,
runPPTemplateTest,
showAddPPTemplateModal,
editPPTemplate,
closePPTemplateModal,
savePPTemplate,
deletePPTemplate,
addFilterFromSelect,
toggleFilterExpand,
removeFilter,
moveFilter,
updateFilterOption,
renderModalFilterList,
cloneStream,
cloneCaptureTemplate,
clonePPTemplate,
showAddCSPTModal,
editCSPT,
closeCSPTModal,
saveCSPT,
deleteCSPT,
cloneCSPT,
csptAddFilterFromSelect,
csptToggleFilterExpand,
csptRemoveFilter,
csptUpdateFilterOption,
renderCSPTModalFilterList,
showAddAudioTemplateModal,
editAudioTemplate,
closeAudioTemplateModal,
saveAudioTemplate,
deleteAudioTemplate,
cloneAudioTemplate,
onAudioEngineChange,
showTestAudioTemplateModal,
closeTestAudioTemplateModal,
startAudioTemplateTest,
// kc-targets
createKCTargetCard,
testKCTarget,
showKCEditor,
closeKCEditorModal,
forceCloseKCEditorModal,
saveKCEditor,
deleteKCTarget,
disconnectAllKCWebSockets,
updateKCBrightnessLabel,
saveKCBrightness,
cloneKCTarget,
// pattern-templates
createPatternTemplateCard,
showPatternTemplateEditor,
closePatternTemplateModal,
forceClosePatternTemplateModal,
savePatternTemplate,
deletePatternTemplate,
renderPatternRectList,
selectPatternRect,
updatePatternRect,
addPatternRect,
deleteSelectedPatternRect,
removePatternRect,
capturePatternBackground,
clonePatternTemplate,
// automations
loadAutomations,
switchAutomationTab,
openAutomationEditor,
closeAutomationEditorModal,
saveAutomationEditor,
addAutomationCondition,
toggleAutomationEnabled,
cloneAutomation,
deleteAutomation,
copyWebhookUrl,
// scene presets (modal buttons stay on window; card actions migrated to event delegation)
openScenePresetCapture,
editScenePreset,
saveScenePreset,
closeScenePresetEditor,
activateScenePreset,
cloneScenePreset,
deleteScenePreset,
addSceneTarget,
// device-discovery
onDeviceTypeChanged,
updateBaudFpsHint,
onSerialPortFocus,
showAddDevice,
closeAddDeviceModal,
scanForDevices,
handleAddDevice,
// targets
loadTargetsTab,
switchTargetSubTab,
showTargetEditor,
closeTargetEditorModal,
forceCloseTargetEditorModal,
saveTargetEditor,
startTargetProcessing,
stopTargetProcessing,
stopAllLedTargets,
stopAllKCTargets,
startTargetOverlay,
stopTargetOverlay,
deleteTarget,
cloneTarget,
toggleLedPreview,
disconnectAllLedPreviewWS,
// color-strip sources
showCSSEditor,
closeCSSEditorModal,
forceCSSEditorClose,
saveCSSEditor,
deleteColorStrip,
onCSSTypeChange,
onEffectTypeChange,
onEffectPaletteChange,
onCSSClockChange,
onAnimationTypeChange,
onDaylightRealTimeChange,
colorCycleAddColor,
colorCycleRemoveColor,
compositeAddLayer,
compositeRemoveLayer,
mappedAddZone,
mappedRemoveZone,
onAudioVizChange,
onGradientPresetChange,
promptAndSaveGradientPreset,
deleteAndRefreshGradientPreset,
showGradientModal,
closeGradientEditor,
saveGradientEntity,
cloneGradient,
editGradient,
deleteGradient,
cloneColorStrip,
toggleCSSOverlay,
previewCSSFromEditor,
copyEndpointUrl,
onNotificationFilterModeChange,
notificationAddAppOverride, notificationRemoveAppOverride,
testNotification,
showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer,
// audio sources
showAudioSourceModal,
closeAudioSourceModal,
saveAudioSource,
editAudioSource,
cloneAudioSource,
deleteAudioSource,
testAudioSource,
closeTestAudioSourceModal,
refreshAudioDevices,
// value sources
showValueSourceModal,
closeValueSourceModal,
saveValueSource,
editValueSource,
cloneValueSource,
deleteValueSource,
onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource,
closeTestValueSourceModal,
// calibration
showCalibration,
closeCalibrationModal,
forceCloseCalibrationModal,
saveCalibration,
updateOffsetSkipLock,
updateCalibrationPreview,
setStartPosition,
toggleEdgeInputs,
toggleDirection,
toggleTestEdge,
showCSSCalibration,
toggleCalibrationOverlay,
// advanced calibration
showAdvancedCalibration,
closeAdvancedCalibration,
saveAdvancedCalibration,
addCalibrationLine,
removeCalibrationLine,
selectCalibrationLine,
moveCalibrationLine,
updateCalibrationLine,
resetCalibrationView,
// graph editor
loadGraphEditor,
toggleGraphLegend,
toggleGraphMinimap,
toggleGraphFilter,
toggleGraphFilterTypes,
toggleGraphHelp,
graphUndo,
graphRedo,
graphFitAll,
graphZoomIn,
graphZoomOut,
graphRelayout,
graphToggleFullscreen,
graphAddEntity,
// tabs / navigation / command palette
switchTab,
startAutoRefresh,
navigateToCard,
openCommandPalette,
closeCommandPalette,
// settings (tabs / backup / restore / auto-backup / MQTT / api keys / log level)
openSettingsModal,
closeSettingsModal,
switchSettingsTab,
downloadBackup,
handleRestoreFileSelected,
saveAutoBackupSettings,
triggerBackupNow,
restoreSavedBackup,
downloadSavedBackup,
deleteSavedBackup,
restartServer,
saveMqttSettings,
loadApiKeysList,
connectLogViewer,
disconnectLogViewer,
clearLogViewer,
applyLogFilter,
openLogOverlay,
closeLogOverlay,
loadLogLevel,
setLogLevel,
saveExternalUrl,
getBaseOrigin,
// update
checkForUpdates,
loadUpdateSettings,
saveUpdateSettings,
dismissUpdate,
initUpdateSettingsPanel,
applyUpdate,
openReleaseNotes,
closeReleaseNotes,
// donation
dismissDonation,
snoozeDonation,
renderAboutPanel,
// appearance
applyStylePreset,
applyBgEffect,
renderAppearanceTab,
});
// ─── Global keyboard shortcuts ───
document.addEventListener('keydown', (e) => {
const tag = document.activeElement?.tagName;
const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
// Command palette: Ctrl+K / Cmd+K (works even in inputs)
if ((e.ctrlKey || e.metaKey) && e.key === 'k' && !e.altKey && !e.shiftKey) {
e.preventDefault();
openCommandPalette();
return;
}
// Tab shortcuts: Ctrl+1..4 (skip when typing in inputs)
if (!inInput && e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams', '5': 'graph' };
const tab = tabMap[e.key];
if (tab) {
e.preventDefault();
switchTab(tab);
return;
}
}
if (e.key === 'Escape') {
// Close in order: log overlay > overlay lightboxes > modals via stack
const logOverlay = queryEl('log-overlay');
if (logOverlay && logOverlay.style.display !== 'none') {
closeLogOverlay();
} else if (queryEl('display-picker-lightbox')?.classList.contains('active')) {
closeDisplayPicker();
} else if (queryEl('image-lightbox')?.classList.contains('active')) {
closeLightbox();
} else {
Modal.closeTopmost();
}
}
});
// ─── Browser back/forward via hash routing ───
window.addEventListener('popstate', handlePopState);
// ─── Cleanup on page unload ───
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
stopConnectionMonitor();
stopEventsWS();
disconnectAllKCWebSockets();
disconnectAllLedPreviewWS();
});
// ─── Initialization ───
document.addEventListener('DOMContentLoaded', async () => {
// Load API key from localStorage before anything that triggers API calls
setApiKey(localStorage.getItem('wled_api_key'));
// Initialize locale (dispatches languageChanged which may trigger API calls)
await initLocale();
// Load external URL setting early so getBaseOrigin() is available for card rendering
loadExternalUrl();
// Restore active tab before showing content to avoid visible jump
initTabs();
// Show content now that translations are loaded and tabs are set
document.body.style.visibility = 'visible';
// Initialize visual effects
initCardGlare();
initBgAnim();
initBgShaders();
initAppearance();
initTabIndicator();
updateBgAnimTheme(document.documentElement.getAttribute('data-theme') !== 'light');
const accent = localStorage.getItem('accentColor') || '#4CAF50';
updateBgAnimAccent(accent);
// Set CSS variable for sticky header height (header now includes tab bar)
const headerEl = document.querySelector('header');
if (headerEl) {
const updateHeaderHeight = () => {
const hh = headerEl.offsetHeight;
document.documentElement.style.setProperty('--header-height', hh + 'px');
document.documentElement.style.setProperty('--sticky-top', hh + 'px');
};
updateHeaderHeight();
window.addEventListener('resize', updateHeaderHeight);
}
// Scroll-to-top button visibility
const scrollBtn = document.getElementById('scroll-to-top');
if (scrollBtn) {
window.addEventListener('scroll', () => {
scrollBtn.classList.toggle('visible', window.scrollY > 300);
}, { passive: true });
}
// Initialize command palette
initCommandPalette();
// Setup form handler
const addDeviceForm = queryEl('add-device-form');
if (addDeviceForm) addDeviceForm.addEventListener('submit', handleAddDevice);
// Always monitor server connection (even before login)
await loadServerInfo();
startConnectionMonitor();
// Expose auth state for inline scripts (after loadServerInfo sets it)
(window as any)._authRequired = authRequired;
if (typeof window.updateAuthUI === 'function') window.updateAuthUI();
// Show login modal only when auth is enabled and no API key is stored
if (authRequired && !apiKey) {
setTimeout(() => {
if (typeof window.showApiKeyModal === 'function') {
window.showApiKeyModal(null, true);
}
}, 100);
return;
}
// User is logged in, load data
loadDisplays();
loadTargetsTab();
// Start global events WebSocket and auto-refresh
startEventsWS();
startEntityEventListeners();
startAutoRefresh();
// Initialize update checker (banner + WS listener)
initUpdateListener();
loadUpdateStatus();
// Show donation banner (after a few sessions)
setProjectUrls(serverRepoUrl, serverDonateUrl);
initDonationBanner();
// Show getting-started tutorial on first visit
if (!localStorage.getItem('tour_completed')) {
setTimeout(() => startGettingStartedTutorial(), 600);
}
});