Add demo mode: virtual hardware sandbox for testing without real devices
Demo mode provides a complete sandbox environment with: - Virtual capture engine (radial rainbow test pattern on 3 displays) - Virtual audio engine (synthetic music-like audio on 2 devices) - Virtual LED device provider (strip/60, matrix/256, ring/24 LEDs) - Isolated data directory (data/demo/) with auto-seeded sample entities - Dedicated config (config/demo_config.yaml) with pre-configured API key - Frontend indicator (DEMO badge + dismissible banner) - Engine filtering (only demo engines visible in demo mode) - Separate entry point: python -m wled_controller.demo (port 8081) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { t } from '../core/i18n.ts';
|
||||
import { showToast, showConfirm, setTabRefreshing } from '../core/ui.ts';
|
||||
import { Modal } from '../core/modal.ts';
|
||||
import { CardSection } from '../core/card-sections.ts';
|
||||
import { updateTabBadge } from './tabs.ts';
|
||||
import { updateTabBadge, updateSubTabHash } from './tabs.ts';
|
||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE, ICON_CLONE, ICON_TRASH, ICON_CIRCLE_OFF } from '../core/icons.ts';
|
||||
import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
@@ -17,6 +17,7 @@ import { getBaseOrigin } from './settings.ts';
|
||||
import { IconSelect } from '../core/icon-select.ts';
|
||||
import { EntitySelect } from '../core/entity-palette.ts';
|
||||
import { attachProcessPicker } from '../core/process-picker.ts';
|
||||
import { TreeNav } from '../core/tree-nav.ts';
|
||||
import { csScenes, createSceneCard } from './scene-presets.ts';
|
||||
import type { Automation } from '../types.ts';
|
||||
|
||||
@@ -85,6 +86,29 @@ const csAutomations = new CardSection('automations', { titleKey: 'automations.ti
|
||||
{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteAutomations },
|
||||
] } as any);
|
||||
|
||||
// ── Tree navigation ──
|
||||
|
||||
let _automationsTreeTriggered = false;
|
||||
|
||||
const _automationsTree = new TreeNav('automations-tree-nav', {
|
||||
onSelect: (key: string) => {
|
||||
_automationsTreeTriggered = true;
|
||||
switchAutomationTab(key);
|
||||
_automationsTreeTriggered = false;
|
||||
}
|
||||
});
|
||||
|
||||
export function switchAutomationTab(tabKey: string) {
|
||||
document.querySelectorAll('.automation-sub-tab-panel').forEach(panel =>
|
||||
(panel as HTMLElement).classList.toggle('active', panel.id === `automation-tab-${tabKey}`)
|
||||
);
|
||||
localStorage.setItem('activeAutomationTab', tabKey);
|
||||
updateSubTabHash('automations', tabKey);
|
||||
if (!_automationsTreeTriggered) {
|
||||
_automationsTree.setActive(tabKey);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Condition logic IconSelect ───────────────────────────────── */
|
||||
|
||||
const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
@@ -147,18 +171,34 @@ function renderAutomations(automations: any, sceneMap: any) {
|
||||
const autoItems = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) })));
|
||||
const sceneItems = csScenes.applySortOrder(scenePresetsCache.data.map(s => ({ key: s.id, html: createSceneCard(s) })));
|
||||
|
||||
const activeTab = localStorage.getItem('activeAutomationTab') || 'automations';
|
||||
|
||||
const treeItems = [
|
||||
{ key: 'automations', icon: ICON_AUTOMATION, titleKey: 'automations.title', count: automations.length },
|
||||
{ key: 'scenes', icon: ICON_SCENE, titleKey: 'scenes.title', count: scenePresetsCache.data.length },
|
||||
];
|
||||
|
||||
if (csAutomations.isMounted()) {
|
||||
_automationsTree.updateCounts({
|
||||
automations: automations.length,
|
||||
scenes: scenePresetsCache.data.length,
|
||||
});
|
||||
csAutomations.reconcile(autoItems);
|
||||
csScenes.reconcile(sceneItems);
|
||||
} else {
|
||||
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
container.innerHTML = toolbar + csAutomations.render(autoItems) + csScenes.render(sceneItems);
|
||||
csAutomations.bind();
|
||||
csScenes.bind();
|
||||
const panels = [
|
||||
{ key: 'automations', html: csAutomations.render(autoItems) },
|
||||
{ key: 'scenes', html: csScenes.render(sceneItems) },
|
||||
].map(p => `<div class="automation-sub-tab-panel stream-tab-panel${p.key === activeTab ? ' active' : ''}" id="automation-tab-${p.key}">${p.html}</div>`).join('');
|
||||
|
||||
// Localize data-i18n elements within the container
|
||||
container.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = t(el.getAttribute('data-i18n'));
|
||||
container.innerHTML = panels;
|
||||
CardSection.bindAll([csAutomations, csScenes]);
|
||||
|
||||
_automationsTree.setExtraHtml(`<button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
|
||||
_automationsTree.update(treeItems, activeTab);
|
||||
_automationsTree.observeSections('automations-content', {
|
||||
'automations': 'automations',
|
||||
'scenes': 'scenes',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user