refactor: comprehensive code quality, security, and release readiness improvements
Lint & Test / test (push) Failing after 48s
Lint & Test / test (push) Failing after 48s
Security: tighten CORS defaults, add webhook rate limiting, fix XSS in automations, guard WebSocket JSON.parse, validate ADB address input, seal debug exception leak, URL-encode WS tokens, CSS.escape in selectors. Code quality: add Pydantic models for brightness/power endpoints, fix thread safety and name uniqueness in DeviceStore, immutable update pattern, split 6 oversized files into 16 focused modules, enable TypeScript strictNullChecks (741→102 errors), type state variables, add dom-utils helper, migrate 3 modules from inline onclick to event delegation, ProcessorDependencies dataclass. Performance: async store saves, health endpoint log level, command palette debounce, optimized entity-events comparison, fix service worker precache list. Testing: expand from 45 to 293 passing tests — add store tests (141), route tests (25), core logic tests (42), E2E flow tests (33), organize into tests/api/, tests/storage/, tests/core/, tests/e2e/. DevOps: CI test pipeline, pre-commit config, Dockerfile multi-stage build with non-root user and health check, docker-compose improvements, version bump to 0.2.0. Docs: rewrite CLAUDE.md (202→56 lines), server/CLAUDE.md (212→76), create contexts/server-operations.md, fix .js→.ts references, fix env var prefix in README, rewrite INSTALLATION.md, add CONTRIBUTING.md and .env.example.
This commit is contained in:
@@ -15,10 +15,11 @@ import { scenePresetsCache, outputTargetsCache, automationsCacheObj } from '../c
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { cardColorStyle, cardColorButton } from '../core/card-colors.ts';
|
||||
import { EntityPalette } from '../core/entity-palette.ts';
|
||||
import { navigateToCard } from '../core/navigation.ts';
|
||||
import type { ScenePreset } from '../types.ts';
|
||||
|
||||
let _editingId: string | null = null;
|
||||
let _allTargets = []; // fetched on capture open
|
||||
let _allTargets: any[] = []; // fetched on capture open
|
||||
let _sceneTagsInput: TagInput | null = null;
|
||||
|
||||
class ScenePresetEditorModal extends Modal {
|
||||
@@ -76,7 +77,7 @@ export function createSceneCard(preset: ScenePreset) {
|
||||
const colorStyle = cardColorStyle(preset.id);
|
||||
return `<div class="card" data-scene-id="${preset.id}"${colorStyle ? ` style="${colorStyle}"` : ''}>
|
||||
<div class="card-top-actions">
|
||||
<button class="card-remove-btn" onclick="deleteScenePreset('${preset.id}', '${escapeHtml(preset.name)}')" title="${t('scenes.delete')}">✕</button>
|
||||
<button class="card-remove-btn" data-action="delete-scene" data-id="${preset.id}" title="${t('scenes.delete')}">✕</button>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<div class="card-title" title="${escapeHtml(preset.name)}">${escapeHtml(preset.name)}</div>
|
||||
@@ -88,10 +89,10 @@ export function createSceneCard(preset: ScenePreset) {
|
||||
</div>
|
||||
${renderTagChips(preset.tags)}
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneScenePreset('${preset.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editScenePreset('${preset.id}')" title="${t('scenes.edit')}">${ICON_EDIT}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="recaptureScenePreset('${preset.id}')" title="${t('scenes.recapture')}">${ICON_REFRESH}</button>
|
||||
<button class="btn btn-icon btn-success" onclick="activateScenePreset('${preset.id}')" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
<button class="btn btn-icon btn-secondary" data-action="clone-scene" data-id="${preset.id}" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" data-action="edit-scene" data-id="${preset.id}" title="${t('scenes.edit')}">${ICON_EDIT}</button>
|
||||
<button class="btn btn-icon btn-secondary" data-action="recapture-scene" data-id="${preset.id}" title="${t('scenes.recapture')}">${ICON_REFRESH}</button>
|
||||
<button class="btn btn-icon btn-success" data-action="activate-scene" data-id="${preset.id}" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
${cardColorButton(preset.id, 'data-scene-id')}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -106,7 +107,7 @@ export async function loadScenePresets(): Promise<ScenePreset[]> {
|
||||
export function renderScenePresetsSection(presets: ScenePreset[]): string | { headerExtra: string; content: string } {
|
||||
if (!presets || presets.length === 0) return '';
|
||||
|
||||
const captureBtn = `<button class="btn btn-sm btn-primary dashboard-stop-all" onclick="event.stopPropagation(); openScenePresetCapture()" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
|
||||
const captureBtn = `<button class="btn btn-sm btn-primary dashboard-stop-all" data-action="capture-scene" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
|
||||
const cards = presets.map(p => _renderDashboardPresetCard(p)).join('');
|
||||
|
||||
return { headerExtra: captureBtn, content: `<div class="dashboard-autostart-grid">${cards}</div>` };
|
||||
@@ -120,7 +121,7 @@ function _renderDashboardPresetCard(preset: ScenePreset): string {
|
||||
].filter(Boolean).join(' \u00b7 ');
|
||||
|
||||
const pStyle = cardColorStyle(preset.id);
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}"${pStyle ? ` style="${pStyle}"` : ''}>
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" data-action="navigate-scene" data-id="${preset.id}"${pStyle ? ` style="${pStyle}"` : ''}>
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_SCENE}</span>
|
||||
<div>
|
||||
@@ -130,7 +131,7 @@ function _renderDashboardPresetCard(preset: ScenePreset): string {
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-target-actions">
|
||||
<button class="dashboard-action-btn start" onclick="activateScenePreset('${preset.id}')" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
<button class="dashboard-action-btn start" data-action="activate-scene" data-id="${preset.id}" title="${t('scenes.activate')}">${ICON_START}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -155,7 +156,7 @@ export async function openScenePresetCapture(): Promise<void> {
|
||||
selectorGroup.style.display = '';
|
||||
targetList.innerHTML = '';
|
||||
try {
|
||||
_allTargets = await outputTargetsCache.fetch().catch(() => []);
|
||||
_allTargets = await outputTargetsCache.fetch().catch((): any[] => []);
|
||||
_refreshTargetSelect();
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
@@ -190,7 +191,7 @@ export async function editScenePreset(presetId: string): Promise<void> {
|
||||
selectorGroup.style.display = '';
|
||||
targetList.innerHTML = '';
|
||||
try {
|
||||
_allTargets = await outputTargetsCache.fetch().catch(() => []);
|
||||
_allTargets = await outputTargetsCache.fetch().catch((): any[] => []);
|
||||
|
||||
// Pre-add targets already in the preset
|
||||
const presetTargetIds = (preset.targets || []).map(pt => pt.target_id || pt.id);
|
||||
@@ -200,7 +201,7 @@ export async function editScenePreset(presetId: string): Promise<void> {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'scene-target-item';
|
||||
item.dataset.targetId = tid;
|
||||
item.innerHTML = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
item.innerHTML = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" data-action="remove-scene-target" title="Remove">✕</button>`;
|
||||
targetList.appendChild(item);
|
||||
}
|
||||
_refreshTargetSelect();
|
||||
@@ -294,7 +295,7 @@ function _addTargetToList(targetId: string, targetName: string): void {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'scene-target-item';
|
||||
item.dataset.targetId = targetId;
|
||||
item.innerHTML = `<span>${ICON_TARGET} ${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
item.innerHTML = `<span>${ICON_TARGET} ${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" data-action="remove-scene-target" title="Remove">✕</button>`;
|
||||
list.appendChild(item);
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
@@ -320,10 +321,7 @@ export async function addSceneTarget(): Promise<void> {
|
||||
if (tgt) _addTargetToList(tgt.id, tgt.name);
|
||||
}
|
||||
|
||||
export function removeSceneTarget(btn: HTMLElement): void {
|
||||
btn.closest('.scene-target-item').remove();
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
// removeSceneTarget is now handled via event delegation on the modal
|
||||
|
||||
// ===== Activate =====
|
||||
|
||||
@@ -403,7 +401,7 @@ export async function cloneScenePreset(presetId: string): Promise<void> {
|
||||
selectorGroup.style.display = '';
|
||||
targetList.innerHTML = '';
|
||||
try {
|
||||
_allTargets = await outputTargetsCache.fetch().catch(() => []);
|
||||
_allTargets = await outputTargetsCache.fetch().catch((): any[] => []);
|
||||
|
||||
// Pre-add targets from the cloned preset
|
||||
const clonedTargetIds = (preset.targets || []).map(pt => pt.target_id || pt.id);
|
||||
@@ -413,7 +411,7 @@ export async function cloneScenePreset(presetId: string): Promise<void> {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'scene-target-item';
|
||||
item.dataset.targetId = tid;
|
||||
item.innerHTML = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
item.innerHTML = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" data-action="remove-scene-target" title="Remove">✕</button>`;
|
||||
targetList.appendChild(item);
|
||||
}
|
||||
_refreshTargetSelect();
|
||||
@@ -456,6 +454,57 @@ export async function deleteScenePreset(presetId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Event delegation for scene preset card actions =====
|
||||
|
||||
const _sceneCardActions: Record<string, (id: string) => void> = {
|
||||
'delete-scene': deleteScenePreset,
|
||||
'clone-scene': cloneScenePreset,
|
||||
'edit-scene': editScenePreset,
|
||||
'recapture-scene': recaptureScenePreset,
|
||||
'activate-scene': activateScenePreset,
|
||||
};
|
||||
|
||||
export function initScenePresetDelegation(container: HTMLElement): void {
|
||||
container.addEventListener('click', (e: MouseEvent) => {
|
||||
const btn = (e.target as HTMLElement).closest<HTMLElement>('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
const id = btn.dataset.id;
|
||||
if (!action) return;
|
||||
|
||||
if (action === 'capture-scene') {
|
||||
e.stopPropagation();
|
||||
openScenePresetCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'navigate-scene') {
|
||||
// Only navigate if click wasn't on a child button
|
||||
if ((e.target as HTMLElement).closest('button')) return;
|
||||
navigateToCard('automations', null, 'scenes', 'data-scene-id', id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'remove-scene-target') {
|
||||
const item = btn.closest('.scene-target-item');
|
||||
if (item) {
|
||||
item.remove();
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!id) return;
|
||||
|
||||
const handler = _sceneCardActions[action];
|
||||
if (handler) {
|
||||
e.stopPropagation();
|
||||
handler(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
function _reloadScenesTab(): void {
|
||||
@@ -466,3 +515,18 @@ function _reloadScenesTab(): void {
|
||||
// Also refresh dashboard (scene presets section)
|
||||
if (typeof window.loadDashboard === 'function') window.loadDashboard(true);
|
||||
}
|
||||
|
||||
// ===== Modal event delegation (for target list remove buttons) =====
|
||||
|
||||
const _sceneEditorModal = document.getElementById('scene-preset-editor-modal');
|
||||
if (_sceneEditorModal) {
|
||||
_sceneEditorModal.addEventListener('click', (e: MouseEvent) => {
|
||||
const btn = (e.target as HTMLElement).closest<HTMLElement>('[data-action="remove-scene-target"]');
|
||||
if (!btn) return;
|
||||
const item = btn.closest('.scene-target-item');
|
||||
if (item) {
|
||||
item.remove();
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user