fix: register pattern-templates API route; add responsive toolbar overflow menu
Lint & Test / test (push) Successful in 2m19s

Pattern templates route existed but was never wired into the router,
dependencies, or database table allowlist — causing 404 on graph tab load.

Graph toolbar now collapses secondary actions into a "more" overflow menu
on viewports narrower than 700px. Primary controls (fit, zoom, add) stay
visible; search, filter, panels, undo/redo, relayout, fullscreen, and
help move into the dropdown.
This commit is contained in:
2026-04-12 21:22:50 +03:00
parent e678e5590a
commit 38f73badbf
7 changed files with 238 additions and 14 deletions
@@ -30,6 +30,7 @@ from .routes.mqtt import router as mqtt_router
from .routes.game_integration import router as game_integration_router from .routes.game_integration import router as game_integration_router
from .routes.audio_processing_templates import router as audio_processing_templates_router from .routes.audio_processing_templates import router as audio_processing_templates_router
from .routes.audio_filters import router as audio_filters_router from .routes.audio_filters import router as audio_filters_router
from .routes.pattern_templates import router as pattern_templates_router
router = APIRouter() router = APIRouter()
router.include_router(system_router) router.include_router(system_router)
@@ -60,5 +61,6 @@ router.include_router(mqtt_router)
router.include_router(game_integration_router) router.include_router(game_integration_router)
router.include_router(audio_processing_templates_router) router.include_router(audio_processing_templates_router)
router.include_router(audio_filters_router) router.include_router(audio_filters_router)
router.include_router(pattern_templates_router)
__all__ = ["router"] __all__ = ["router"]
@@ -38,6 +38,7 @@ from wled_controller.core.game_integration.event_bus import GameEventBus
from wled_controller.storage.mqtt_source_store import MQTTSourceStore from wled_controller.storage.mqtt_source_store import MQTTSourceStore
from wled_controller.core.mqtt.mqtt_manager import MQTTManager from wled_controller.core.mqtt.mqtt_manager import MQTTManager
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
from wled_controller.storage.pattern_template_store import PatternTemplateStore
T = TypeVar("T") T = TypeVar("T")
@@ -168,6 +169,10 @@ def get_audio_processing_template_store() -> AudioProcessingTemplateStore:
return _get("audio_processing_template_store", "Audio processing template store") return _get("audio_processing_template_store", "Audio processing template store")
def get_pattern_template_store() -> PatternTemplateStore:
return _get("pattern_template_store", "Pattern template store")
def get_database() -> Database: def get_database() -> Database:
return _get("database", "Database") return _get("database", "Database")
@@ -233,6 +238,7 @@ def init_dependencies(
mqtt_store: MQTTSourceStore | None = None, mqtt_store: MQTTSourceStore | None = None,
mqtt_manager: MQTTManager | None = None, mqtt_manager: MQTTManager | None = None,
audio_processing_template_store: AudioProcessingTemplateStore | None = None, audio_processing_template_store: AudioProcessingTemplateStore | None = None,
pattern_template_store: PatternTemplateStore | None = None,
): ):
"""Initialize global dependencies.""" """Initialize global dependencies."""
_deps.update( _deps.update(
@@ -267,5 +273,6 @@ def init_dependencies(
"mqtt_store": mqtt_store, "mqtt_store": mqtt_store,
"mqtt_manager": mqtt_manager, "mqtt_manager": mqtt_manager,
"audio_processing_template_store": audio_processing_template_store, "audio_processing_template_store": audio_processing_template_store,
"pattern_template_store": pattern_template_store,
} }
) )
+3
View File
@@ -52,6 +52,7 @@ from wled_controller.core.mqtt.mqtt_service import MQTTService
from wled_controller.core.mqtt.mqtt_manager import MQTTManager from wled_controller.core.mqtt.mqtt_manager import MQTTManager
from wled_controller.storage.mqtt_source_store import MQTTSourceStore from wled_controller.storage.mqtt_source_store import MQTTSourceStore
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
from wled_controller.storage.pattern_template_store import PatternTemplateStore
import wled_controller.core.audio.filters # noqa: F401 — trigger audio filter auto-registration import wled_controller.core.audio.filters # noqa: F401 — trigger audio filter auto-registration
from wled_controller.core.devices.mqtt_client import set_mqtt_service from wled_controller.core.devices.mqtt_client import set_mqtt_service
from wled_controller.core.backup.auto_backup import AutoBackupEngine from wled_controller.core.backup.auto_backup import AutoBackupEngine
@@ -108,6 +109,7 @@ ha_manager = HomeAssistantManager(ha_store)
mqtt_source_store = MQTTSourceStore(db) mqtt_source_store = MQTTSourceStore(db)
audio_processing_template_store = AudioProcessingTemplateStore(db) audio_processing_template_store = AudioProcessingTemplateStore(db)
game_integration_store = GameIntegrationStore(db) game_integration_store = GameIntegrationStore(db)
pattern_template_store = PatternTemplateStore(db)
game_event_bus = GameEventBus() game_event_bus = GameEventBus()
register_community_adapters() register_community_adapters()
@@ -236,6 +238,7 @@ async def lifespan(app: FastAPI):
mqtt_store=mqtt_source_store, mqtt_store=mqtt_source_store,
mqtt_manager=mqtt_manager, mqtt_manager=mqtt_manager,
audio_processing_template_store=audio_processing_template_store, audio_processing_template_store=audio_processing_template_store,
pattern_template_store=pattern_template_store,
) )
# Register devices in processor manager for health monitoring # Register devices in processor manager for health monitoring
@@ -147,6 +147,90 @@ html:has(#tab-graph.active) {
margin: 4px 2px; margin: 4px 2px;
} }
/* ── Toolbar overflow button (hidden on wide screens) ── */
.graph-tb-overflow-btn {
display: none;
}
/* ── Toolbar overflow menu ── */
.graph-overflow-menu {
display: none;
position: absolute;
z-index: 25;
min-width: 170px;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 4px;
box-shadow: 0 4px 16px var(--shadow-color);
flex-direction: column;
gap: 2px;
}
.graph-overflow-menu.open {
display: flex;
}
.graph-overflow-menu.flip-up {
transform: translateY(-100%);
}
.graph-overflow-menu button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 10px;
border: none;
background: transparent;
color: var(--text-color);
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
white-space: nowrap;
transition: background 0.15s;
}
.graph-overflow-menu button:hover {
background: var(--bg-secondary);
}
.graph-overflow-menu button.active {
background: var(--primary-color);
color: var(--primary-contrast);
}
.graph-overflow-menu button:disabled {
opacity: 0.3;
cursor: default;
pointer-events: none;
}
.graph-overflow-menu .icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.graph-overflow-sep {
height: 1px;
background: var(--border-color);
margin: 2px 4px;
}
/* ── Responsive: collapse toolbar on narrow viewports ── */
@media (max-width: 700px) {
.graph-toolbar [data-collapse] {
display: none;
}
.graph-tb-overflow-btn {
display: flex;
}
}
/* ── Legend panel ── */ /* ── Legend panel ── */
.graph-legend { .graph-legend {
+3 -1
View File
@@ -193,7 +193,7 @@ import {
loadGraphEditor, loadGraphEditor,
toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo, toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo,
graphFitAll, graphZoomIn, graphZoomOut, graphRelayout, graphFitAll, graphZoomIn, graphZoomOut, graphRelayout,
graphToggleFullscreen, graphAddEntity, graphToggleFullscreen, graphAddEntity, toggleToolbarOverflow, closeToolbarOverflow,
} from './features/graph-editor.ts'; } from './features/graph-editor.ts';
// Layer 6: tabs, navigation, command palette, settings // Layer 6: tabs, navigation, command palette, settings
@@ -575,6 +575,8 @@ Object.assign(window, {
graphRelayout, graphRelayout,
graphToggleFullscreen, graphToggleFullscreen,
graphAddEntity, graphAddEntity,
toggleToolbarOverflow,
closeToolbarOverflow,
// tabs / navigation / command palette // tabs / navigation / command palette
switchTab, switchTab,
@@ -535,6 +535,86 @@ export function graphFitAll(): void {
export function graphZoomIn(): void { if (_canvas) _canvas.zoomIn(); } export function graphZoomIn(): void { if (_canvas) _canvas.zoomIn(); }
export function graphZoomOut(): void { if (_canvas) _canvas.zoomOut(); } export function graphZoomOut(): void { if (_canvas) _canvas.zoomOut(); }
/* ── Toolbar overflow menu ──────────────────────────────── */
export function toggleToolbarOverflow(): void {
const menu = document.getElementById('graph-overflow-menu');
if (!menu) return;
const open = menu.classList.toggle('open');
if (open) {
_syncOverflowState();
// Position menu near the overflow button
const btn = document.querySelector('.graph-tb-overflow-btn') as HTMLElement | null;
const container = menu.parentElement;
if (btn && container) {
const btnRect = btn.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const menuWidth = 180;
// Default: below the button, right-aligned
let top = btnRect.bottom - containerRect.top + 4;
let left = btnRect.right - containerRect.left - menuWidth;
// Flip above if would overflow bottom
if (top + 240 > containerRect.height) {
top = btnRect.top - containerRect.top - 4;
menu.classList.add('flip-up');
} else {
menu.classList.remove('flip-up');
}
// Clamp left edge
if (left < 4) left = 4;
menu.style.top = `${top}px`;
menu.style.left = `${left}px`;
}
requestAnimationFrame(() => {
document.addEventListener('pointerdown', _closeOverflowOutside, { once: true });
});
}
}
export function closeToolbarOverflow(): void {
const menu = document.getElementById('graph-overflow-menu');
if (menu) menu.classList.remove('open');
}
function _closeOverflowOutside(e: Event): void {
const menu = document.getElementById('graph-overflow-menu');
if (!menu || !menu.classList.contains('open')) return;
const target = e.target as Element;
if (menu.contains(target) || target.closest('.graph-tb-overflow-btn')) {
// Re-listen if click was inside
document.addEventListener('pointerdown', _closeOverflowOutside, { once: true });
return;
}
closeToolbarOverflow();
}
function _syncOverflowState(): void {
// Sync disabled state for undo/redo
const undoBtn = document.getElementById('graph-undo-btn') as HTMLButtonElement | null;
const redoBtn = document.getElementById('graph-redo-btn') as HTMLButtonElement | null;
const overflowUndo = document.getElementById('graph-overflow-undo') as HTMLButtonElement | null;
const overflowRedo = document.getElementById('graph-overflow-redo') as HTMLButtonElement | null;
if (undoBtn && overflowUndo) overflowUndo.disabled = undoBtn.disabled;
if (redoBtn && overflowRedo) overflowRedo.disabled = redoBtn.disabled;
// Sync active state for toggle buttons
const pairs: [string, string][] = [
['graph-legend-toggle', 'graph-overflow-legend'],
['graph-minimap-toggle', 'graph-overflow-minimap'],
['graph-help-toggle', 'graph-overflow-help'],
];
for (const [tbId, ovId] of pairs) {
const tb = document.getElementById(tbId);
const ov = document.getElementById(ovId);
if (tb && ov) ov.classList.toggle('active', tb.classList.contains('active'));
}
// Sync filter active state
const filterTb = document.querySelector('.graph-filter-btn');
const filterOv = document.querySelector('.graph-overflow-filter-btn');
if (filterTb && filterOv) {
filterOv.classList.toggle('active', filterTb.classList.contains('active'));
}
}
export function graphToggleFullscreen(): void { export function graphToggleFullscreen(): void {
const container = document.querySelector('#graph-editor-content .graph-container'); const container = document.querySelector('#graph-editor-content .graph-container');
if (!container) return; if (!container) return;
@@ -929,40 +1009,85 @@ function _graphHTML(): string {
<button class="btn-icon" onclick="graphZoomOut()" title="${t('graph.zoom_out')}"> <button class="btn-icon" onclick="graphZoomOut()" title="${t('graph.zoom_out')}">
<svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg> <svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg>
</button> </button>
<span class="graph-toolbar-sep"></span> <span class="graph-toolbar-sep" data-collapse></span>
<button class="btn-icon" onclick="openCommandPalette()" title="${t('graph.search')} (/)"> <button class="btn-icon" onclick="openCommandPalette()" title="${t('graph.search')} (/)" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
</button> </button>
<button class="btn-icon graph-filter-btn" onclick="toggleGraphFilter()" title="${t('graph.filter')} (F)"> <button class="btn-icon graph-filter-btn" onclick="toggleGraphFilter()" title="${t('graph.filter')} (F)" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg> <svg class="icon" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
</button> </button>
<button class="btn-icon${_legendVisible ? ' active' : ''}" id="graph-legend-toggle" onclick="toggleGraphLegend()" title="${t('graph.legend')}"> <button class="btn-icon${_legendVisible ? ' active' : ''}" id="graph-legend-toggle" onclick="toggleGraphLegend()" title="${t('graph.legend')}" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
</button> </button>
<button class="btn-icon${_minimapVisible ? ' active' : ''}" id="graph-minimap-toggle" onclick="toggleGraphMinimap()" title="${t('graph.minimap')}"> <button class="btn-icon${_minimapVisible ? ' active' : ''}" id="graph-minimap-toggle" onclick="toggleGraphMinimap()" title="${t('graph.minimap')}" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><rect width="7" height="5" x="14" y="14" rx="1" fill="currentColor" opacity="0.3"/></svg> <svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><rect width="7" height="5" x="14" y="14" rx="1" fill="currentColor" opacity="0.3"/></svg>
</button> </button>
<span class="graph-toolbar-sep"></span> <span class="graph-toolbar-sep" data-collapse></span>
<button class="btn-icon" id="graph-undo-btn" onclick="graphUndo()" title="${t('graph.help.undo') || 'Undo'} (Ctrl+Z)" disabled> <button class="btn-icon" id="graph-undo-btn" onclick="graphUndo()" title="${t('graph.help.undo') || 'Undo'} (Ctrl+Z)" disabled data-collapse>
<svg class="icon" viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
</button> </button>
<button class="btn-icon" id="graph-redo-btn" onclick="graphRedo()" title="${t('graph.help.redo') || 'Redo'} (Ctrl+Shift+Z)" disabled> <button class="btn-icon" id="graph-redo-btn" onclick="graphRedo()" title="${t('graph.help.redo') || 'Redo'} (Ctrl+Shift+Z)" disabled data-collapse>
<svg class="icon" viewBox="0 0 24 24"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>
</button> </button>
<span class="graph-toolbar-sep"></span> <span class="graph-toolbar-sep" data-collapse></span>
<button class="btn-icon" onclick="graphRelayout()" title="${t('graph.relayout')}"> <button class="btn-icon" onclick="graphRelayout()" title="${t('graph.relayout')}" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
</button> </button>
<button class="btn-icon" onclick="graphToggleFullscreen()" title="${t('graph.fullscreen')} (F11)"> <button class="btn-icon" onclick="graphToggleFullscreen()" title="${t('graph.fullscreen')} (F11)" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
</button> </button>
<span class="graph-toolbar-sep"></span> <span class="graph-toolbar-sep" data-collapse></span>
<button class="btn-icon" onclick="graphAddEntity()" title="${t('graph.add_entity')} (+)"> <button class="btn-icon" onclick="graphAddEntity()" title="${t('graph.add_entity')} (+)">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
</button> </button>
<button class="btn-icon${_helpVisible ? ' active' : ''}" id="graph-help-toggle" onclick="toggleGraphHelp()" title="${t('graph.help_title') || 'Shortcuts'} (?)"> <button class="btn-icon${_helpVisible ? ' active' : ''}" id="graph-help-toggle" onclick="toggleGraphHelp()" title="${t('graph.help_title') || 'Shortcuts'} (?)" data-collapse>
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg> <svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
</button> </button>
<button class="btn-icon graph-tb-overflow-btn" onclick="toggleToolbarOverflow()" title="${t('graph.more') || 'More'}">
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
</button>
</div>
<div class="graph-overflow-menu" id="graph-overflow-menu">
<button onclick="openCommandPalette(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
<span>${t('graph.search')}</span>
</button>
<button class="graph-overflow-filter-btn" onclick="toggleGraphFilter(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
<span>${t('graph.filter')}</span>
</button>
<button id="graph-overflow-legend" onclick="toggleGraphLegend(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
<span>${t('graph.legend')}</span>
</button>
<button id="graph-overflow-minimap" onclick="toggleGraphMinimap(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><rect width="7" height="5" x="14" y="14" rx="1" fill="currentColor" opacity="0.3"/></svg>
<span>${t('graph.minimap')}</span>
</button>
<div class="graph-overflow-sep"></div>
<button id="graph-overflow-undo" onclick="graphUndo(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
<span>${t('graph.help.undo') || 'Undo'}</span>
</button>
<button id="graph-overflow-redo" onclick="graphRedo(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>
<span>${t('graph.help.redo') || 'Redo'}</span>
</button>
<div class="graph-overflow-sep"></div>
<button onclick="graphRelayout(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
<span>${t('graph.relayout')}</span>
</button>
<button onclick="graphToggleFullscreen(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
<span>${t('graph.fullscreen')}</span>
</button>
<div class="graph-overflow-sep"></div>
<button id="graph-overflow-help" onclick="toggleGraphHelp(); closeToolbarOverflow()">
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
<span>${t('graph.help_title') || 'Shortcuts'}</span>
</button>
</div> </div>
<div class="graph-legend${_legendVisible ? ' visible' : ''}"> <div class="graph-legend${_legendVisible ? ' visible' : ''}">
@@ -59,6 +59,7 @@ _ENTITY_TABLES = [
"mqtt_sources", "mqtt_sources",
"game_integrations", "game_integrations",
"audio_processing_templates", "audio_processing_templates",
"pattern_templates",
] ]