Files
ledgrab/server/src/wled_controller/static/js/app.js
T
alexei.dolgolyov 122e95545c Card bulk operations, remove expand/collapse, graph color picker fix
- Bulk selection mode: Ctrl+Click or toggle button to enter, Escape to exit
- Shift+Click for range select, bottom toolbar with SVG icon action buttons
- All CardSections wired with bulk actions: Delete everywhere,
  Start/Stop for targets, Enable/Disable for automations
- Remove expand/collapse all buttons (no collapsible sections remain)
- Fix graph node color picker overlay persisting after outside click
- Add Icons section to frontend.md conventions
- Add trash2, listChecks, circleOff icons to icon system
- Backend: processing loop performance improvements (monotonic timestamps,
  deque-based FPS tracking)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 01:21:27 +03:00

683 lines
21 KiB
JavaScript

/**
* Entry point — imports all modules, registers globals, initializes app.
*/
// Layer 0: state
import { apiKey, setApiKey, refreshInterval } from './core/state.js';
import { Modal } from './core/modal.js';
// Layer 1: api, i18n
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.js';
import { t, initLocale, changeLocale } from './core/i18n.js';
// Layer 1.5: visual effects
import { initCardGlare } from './core/card-glare.js';
import { initBgAnim, updateBgAnimAccent, updateBgAnimTheme } from './core/bg-anim.js';
import { initTabIndicator, updateTabIndicator } from './core/tab-indicator.js';
// Layer 2: ui
import {
toggleHint, lockBody, unlockBody, closeLightbox,
showToast, showConfirm, closeConfirmModal,
openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner,
} from './core/ui.js';
// Layer 3: displays, tutorials
import {
openDisplayPicker, closeDisplayPicker, selectDisplay, formatDisplayLabel,
} from './features/displays.js';
import {
startCalibrationTutorial, startDeviceTutorial, startGettingStartedTutorial,
startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startAutomationsTutorial,
closeTutorial, tutorialNext, tutorialPrev,
} from './features/tutorials.js';
// 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.js';
import {
loadDashboard, stopUptimeTimer,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.js';
import { startEventsWS, stopEventsWS } from './core/events-ws.js';
import { startEntityEventListeners } from './core/entity-events.js';
import {
startPerfPolling, stopPerfPolling,
} from './features/perf-charts.js';
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.js';
import {
createKCTargetCard, testKCTarget,
showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor,
deleteKCTarget, disconnectAllKCWebSockets,
updateKCBrightnessLabel, saveKCBrightness,
cloneKCTarget,
} from './features/kc-targets.js';
import {
createPatternTemplateCard,
showPatternTemplateEditor, closePatternTemplateModal, forceClosePatternTemplateModal,
savePatternTemplate, deletePatternTemplate,
renderPatternRectList, selectPatternRect, updatePatternRect,
addPatternRect, deleteSelectedPatternRect, removePatternRect,
capturePatternBackground,
clonePatternTemplate,
} from './features/pattern-templates.js';
import {
loadAutomations, openAutomationEditor, closeAutomationEditorModal,
saveAutomationEditor, addAutomationCondition,
toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl,
} from './features/automations.js';
import {
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
activateScenePreset, recaptureScenePreset, cloneScenePreset, deleteScenePreset,
addSceneTarget, removeSceneTarget,
} from './features/scene-presets.js';
// Layer 5: device-discovery, targets
import {
onDeviceTypeChanged, updateBaudFpsHint, onSerialPortFocus,
showAddDevice, closeAddDeviceModal, scanForDevices, handleAddDevice,
cloneDevice,
} from './features/device-discovery.js';
import {
loadTargetsTab, switchTargetSubTab,
showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
startTargetProcessing, stopTargetProcessing,
stopAllLedTargets, stopAllKCTargets,
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview,
disconnectAllLedPreviewWS,
} from './features/targets.js';
// Layer 5: color-strip sources
import {
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
onCSSTypeChange, onEffectTypeChange, onAnimationTypeChange, onCSSClockChange, onDaylightRealTimeChange,
colorCycleAddColor, colorCycleRemoveColor,
compositeAddLayer, compositeRemoveLayer,
mappedAddZone, mappedRemoveZone,
onAudioVizChange,
applyGradientPreset,
onGradientPresetChange,
promptAndSaveGradientPreset,
applyCustomGradientPreset,
deleteAndRefreshGradientPreset,
cloneColorStrip,
toggleCSSOverlay,
previewCSSFromEditor,
copyEndpointUrl,
onNotificationFilterModeChange,
notificationAddAppColor, notificationRemoveAppColor,
testNotification,
showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer,
} from './features/color-strips.js';
// Layer 5: audio sources
import {
showAudioSourceModal, closeAudioSourceModal, saveAudioSource,
editAudioSource, cloneAudioSource, deleteAudioSource,
testAudioSource, closeTestAudioSourceModal,
refreshAudioDevices,
} from './features/audio-sources.js';
// Layer 5: value sources
import {
showValueSourceModal, closeValueSourceModal, saveValueSource,
editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource, closeTestValueSourceModal,
} from './features/value-sources.js';
// Layer 5: calibration
import {
showCalibration, closeCalibrationModal, forceCloseCalibrationModal, saveCalibration,
updateOffsetSkipLock, updateCalibrationPreview,
setStartPosition, toggleEdgeInputs, toggleDirection, toggleTestEdge,
showCSSCalibration, toggleCalibrationOverlay,
} from './features/calibration.js';
import {
showAdvancedCalibration, closeAdvancedCalibration, saveAdvancedCalibration,
addCalibrationLine, removeCalibrationLine, selectCalibrationLine, moveCalibrationLine,
updateCalibrationLine, resetCalibrationView,
} from './features/advanced-calibration.js';
// Layer 5.5: graph editor
import {
loadGraphEditor,
toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo,
graphFitAll, graphZoomIn, graphZoomOut, graphRelayout,
graphToggleFullscreen, graphAddEntity,
} from './features/graph-editor.js';
// Layer 6: tabs, navigation, command palette, settings
import { switchTab, initTabs, startAutoRefresh, handlePopState } from './features/tabs.js';
import { navigateToCard } from './core/navigation.js';
import { openCommandPalette, closeCommandPalette, initCommandPalette } from './core/command-palette.js';
import {
openSettingsModal, closeSettingsModal, switchSettingsTab,
downloadBackup, handleRestoreFileSelected,
saveAutoBackupSettings, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
restartServer, saveMqttSettings,
loadApiKeysList,
downloadPartialExport, handlePartialImportFileSelected,
connectLogViewer, disconnectLogViewer, clearLogViewer, applyLogFilter,
openLogOverlay, closeLogOverlay,
loadLogLevel, setLogLevel,
saveExternalUrl, getBaseOrigin, loadExternalUrl,
} from './features/settings.js';
// ─── 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,
showConfirm,
closeConfirmModal,
openFullImageLightbox,
showOverlaySpinner,
hideOverlaySpinner,
// 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,
// 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,
openAutomationEditor,
closeAutomationEditorModal,
saveAutomationEditor,
addAutomationCondition,
toggleAutomationEnabled,
cloneAutomation,
deleteAutomation,
copyWebhookUrl,
// scene presets
openScenePresetCapture,
editScenePreset,
saveScenePreset,
closeScenePresetEditor,
activateScenePreset,
recaptureScenePreset,
cloneScenePreset,
deleteScenePreset,
addSceneTarget,
removeSceneTarget,
// 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,
onCSSClockChange,
onAnimationTypeChange,
onDaylightRealTimeChange,
colorCycleAddColor,
colorCycleRemoveColor,
compositeAddLayer,
compositeRemoveLayer,
mappedAddZone,
mappedRemoveZone,
onAudioVizChange,
applyGradientPreset,
onGradientPresetChange,
promptAndSaveGradientPreset,
applyCustomGradientPreset,
deleteAndRefreshGradientPreset,
cloneColorStrip,
toggleCSSOverlay,
previewCSSFromEditor,
copyEndpointUrl,
onNotificationFilterModeChange,
notificationAddAppColor, notificationRemoveAppColor,
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 / partial export-import / api keys / log level)
openSettingsModal,
closeSettingsModal,
switchSettingsTab,
downloadBackup,
handleRestoreFileSelected,
saveAutoBackupSettings,
restoreSavedBackup,
downloadSavedBackup,
deleteSavedBackup,
restartServer,
saveMqttSettings,
loadApiKeysList,
downloadPartialExport,
handlePartialImportFileSelected,
connectLogViewer,
disconnectLogViewer,
clearLogViewer,
applyLogFilter,
openLogOverlay,
closeLogOverlay,
loadLogLevel,
setLogLevel,
saveExternalUrl,
getBaseOrigin,
});
// ─── 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 = document.getElementById('log-overlay');
if (logOverlay && logOverlay.style.display !== 'none') {
closeLogOverlay();
} else if (document.getElementById('display-picker-lightbox').classList.contains('active')) {
closeDisplayPicker();
} else if (document.getElementById('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();
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
document.getElementById('add-device-form').addEventListener('submit', handleAddDevice);
// Always monitor server connection (even before login)
loadServerInfo();
startConnectionMonitor();
// Show modal if no API key is stored
if (!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();
// Show getting-started tutorial on first visit
if (!localStorage.getItem('tour_completed')) {
setTimeout(() => startGettingStartedTutorial(), 600);
}
});