Files
ledgrab/server/src/wled_controller/static/js/app.js
T
alexei.dolgolyov fa81d6a608 Add WebSocket device type, capability-driven settings, hide filter on collapse
- New WS device type: broadcaster singleton + LEDClient that sends binary
  frames to connected WebSocket clients during processing
- FastAPI WS endpoint at /api/v1/devices/{device_id}/ws with token auth
- Frontend: add/edit WS devices, connection URL with copy button in settings
- Add health_check and auto_restore capabilities to WLED and Serial providers;
  hide health interval and auto-restore toggle for devices without them
- Skip health check loop for virtual devices (Mock, MQTT, WS) — set always-online
- Copy buttons and labels for API CSS push endpoints (REST POST / WebSocket)
- Hide mock:// and ws:// URLs in target device dropdown
- Hide filter textbox when card section is collapsed (cs-collapsed CSS class)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:55:09 +03:00

537 lines
16 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 } from './core/api.js';
import { t, initLocale, changeLocale } from './core/i18n.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, removeDevice, loadDevices,
updateSettingsBaudFpsHint, copyWsUrl,
} from './features/devices.js';
import {
loadDashboard, stopUptimeTimer,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll,
toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.js';
import { startEventsWS, stopEventsWS } from './core/events-ws.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,
expandAllStreamSections, collapseAllStreamSections,
} from './features/streams.js';
import {
createKCTargetCard, testKCTarget, toggleKCTestAutoRefresh,
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, deleteAutomation,
expandAllAutomationSections, collapseAllAutomationSections,
} from './features/automations.js';
import {
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
activateScenePreset, recaptureScenePreset, deleteScenePreset,
addSceneTarget, removeSceneTarget,
} from './features/scene-presets.js';
// Layer 5: device-discovery, targets
import {
onDeviceTypeChanged, updateBaudFpsHint, onSerialPortFocus,
showAddDevice, closeAddDeviceModal, scanForDevices, handleAddDevice,
} from './features/device-discovery.js';
import {
loadTargetsTab, switchTargetSubTab,
showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
startTargetProcessing, stopTargetProcessing,
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview, toggleTargetAutoStart,
expandAllTargetSections, collapseAllTargetSections,
disconnectAllLedPreviewWS,
} from './features/targets.js';
// Layer 5: color-strip sources
import {
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
onCSSTypeChange, onEffectTypeChange, onAnimationTypeChange, updateEffectPreview,
colorCycleAddColor, colorCycleRemoveColor,
compositeAddLayer, compositeRemoveLayer,
mappedAddZone, mappedRemoveZone,
onAudioVizChange,
applyGradientPreset,
cloneColorStrip,
copyEndpointUrl,
} from './features/color-strips.js';
// Layer 5: audio sources
import {
showAudioSourceModal, closeAudioSourceModal, saveAudioSource,
editAudioSource, cloneAudioSource, deleteAudioSource,
testAudioSource, closeTestAudioSourceModal,
} from './features/audio-sources.js';
// Layer 5: value sources
import {
showValueSourceModal, closeValueSourceModal, saveValueSource,
editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
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';
// 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, downloadBackup, handleRestoreFileSelected,
saveAutoBackupSettings, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
} from './features/settings.js';
// ─── Register all HTML onclick / onchange / onfocus globals ───
Object.assign(window, {
// core / state (for inline script)
setApiKey,
// 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,
removeDevice,
loadDevices,
updateSettingsBaudFpsHint,
copyWsUrl,
// dashboard
loadDashboard,
dashboardToggleAutomation,
dashboardStartTarget,
dashboardStopTarget,
dashboardToggleAutoStart,
dashboardStopAll,
toggleDashboardSection,
changeDashboardPollInterval,
stopUptimeTimer,
startPerfPolling,
stopPerfPolling,
// streams / capture templates / PP templates
loadPictureSources,
switchStreamTab,
expandAllStreamSections, collapseAllStreamSections,
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,
showAddAudioTemplateModal,
editAudioTemplate,
closeAudioTemplateModal,
saveAudioTemplate,
deleteAudioTemplate,
cloneAudioTemplate,
onAudioEngineChange,
showTestAudioTemplateModal,
closeTestAudioTemplateModal,
startAudioTemplateTest,
// kc-targets
createKCTargetCard,
testKCTarget,
toggleKCTestAutoRefresh,
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,
deleteAutomation,
expandAllAutomationSections,
collapseAllAutomationSections,
// scene presets
openScenePresetCapture,
editScenePreset,
saveScenePreset,
closeScenePresetEditor,
activateScenePreset,
recaptureScenePreset,
deleteScenePreset,
addSceneTarget,
removeSceneTarget,
// device-discovery
onDeviceTypeChanged,
updateBaudFpsHint,
onSerialPortFocus,
showAddDevice,
closeAddDeviceModal,
scanForDevices,
handleAddDevice,
// targets
loadTargetsTab,
switchTargetSubTab,
expandAllTargetSections, collapseAllTargetSections,
showTargetEditor,
closeTargetEditorModal,
forceCloseTargetEditorModal,
saveTargetEditor,
startTargetProcessing,
stopTargetProcessing,
startTargetOverlay,
stopTargetOverlay,
deleteTarget,
cloneTarget,
toggleLedPreview,
toggleTargetAutoStart,
disconnectAllLedPreviewWS,
// color-strip sources
showCSSEditor,
closeCSSEditorModal,
forceCSSEditorClose,
saveCSSEditor,
deleteColorStrip,
onCSSTypeChange,
onEffectTypeChange,
onAnimationTypeChange,
updateEffectPreview,
colorCycleAddColor,
colorCycleRemoveColor,
compositeAddLayer,
compositeRemoveLayer,
mappedAddZone,
mappedRemoveZone,
onAudioVizChange,
applyGradientPreset,
cloneColorStrip,
copyEndpointUrl,
// audio sources
showAudioSourceModal,
closeAudioSourceModal,
saveAudioSource,
editAudioSource,
cloneAudioSource,
deleteAudioSource,
testAudioSource,
closeTestAudioSourceModal,
// value sources
showValueSourceModal,
closeValueSourceModal,
saveValueSource,
editValueSource,
cloneValueSource,
deleteValueSource,
onValueSourceTypeChange,
addSchedulePoint,
testValueSource,
closeTestValueSourceModal,
// calibration
showCalibration,
closeCalibrationModal,
forceCloseCalibrationModal,
saveCalibration,
updateOffsetSkipLock,
updateCalibrationPreview,
setStartPosition,
toggleEdgeInputs,
toggleDirection,
toggleTestEdge,
showCSSCalibration,
toggleCalibrationOverlay,
// tabs / navigation / command palette
switchTab,
startAutoRefresh,
navigateToCard,
openCommandPalette,
closeCommandPalette,
// settings (backup / restore / auto-backup)
openSettingsModal,
closeSettingsModal,
downloadBackup,
handleRestoreFileSelected,
saveAutoBackupSettings,
restoreSavedBackup,
downloadSavedBackup,
deleteSavedBackup,
});
// ─── 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' };
const tab = tabMap[e.key];
if (tab) {
e.preventDefault();
switchTab(tab);
return;
}
}
if (e.key === 'Escape') {
// Close in order: overlay lightboxes first, then modals via stack
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);
}
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();
// 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';
// Set CSS variable for sticky header height so section headers stack below it
const headerEl = document.querySelector('header');
if (headerEl) {
const updateHeaderHeight = () => {
document.documentElement.style.setProperty(
'--header-height', headerEl.offsetHeight + 'px'
);
};
updateHeaderHeight();
window.addEventListener('resize', updateHeaderHeight);
}
// Initialize command palette
initCommandPalette();
// Setup form handler
document.getElementById('add-device-form').addEventListener('submit', handleAddDevice);
// 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
loadServerInfo();
loadDisplays();
loadTargetsTab();
// Start global events WebSocket and auto-refresh
startEventsWS();
startAutoRefresh();
// Show getting-started tutorial on first visit
if (!localStorage.getItem('tour_completed')) {
setTimeout(() => startGettingStartedTutorial(), 600);
}
});