Comprehensive WebUI review: 41 UX/feature/CSS improvements

Safety & Correctness:
- Add confirmation dialogs to Stop All, turnOffDevice
- i18n confirm dialog (title, yes, no buttons)
- Fix duplicate tutorial-overlay ID
- Define missing CSS variables (--radius, --text-primary, --hover-bg, --input-bg)
- Fix toast z-index conflict with confirm dialog (2500 → 3000)

UX Consistency:
- Add backdrop-close to test modals
- Add device clone feature (only entity without it)
- Add sync clocks to command palette
- Replace 20+ hardcoded accent colors with CSS vars/color-mix()
- Remove dead .badge duplicate from components.css
- Make calibration elements keyboard-accessible (div → button)
- Add aria-labels to color picker swatches
- Fix pattern canvas mobile horizontal scroll
- Fix graph editor mobile bottom clipping

Polish:
- Add empty-state messages to all CardSection instances
- Convert 21 px font-sizes to rem
- Add scroll-behavior: smooth with reduced-motion override
- Add @media print styles
- Add :focus-visible to 4 missing interactive elements
- Fix settings modal close label (Cancel → Close)
- Fix api-key submit button i18n

New Features:
- Command palette actions: start/stop targets, activate scenes, enable/disable
- Bulk start/stop API endpoints (POST /output-targets/bulk/start|stop)
- OS notification history viewer modal
- Scene "used by" automation reference count on cards
- Clock elapsed time display on Streams tab cards
- Device "last seen" relative timestamp on cards
- Audio device refresh button in edit modal
- Composite layer drag-to-reorder
- MQTT settings panel (broker config with JSON persistence)
- WebSocket log viewer with level filtering and ring buffer
- Runtime log-level adjustment (GET/PUT endpoints + settings UI)
- Animated value source waveform canvas preview
- Gradient custom preset save/delete (localStorage)
- API key read-only display in settings
- Backup metadata (file size, auto/manual badges)
- Server restart button with confirm + overlay
- Partial config export/import per entity type
- Progressive disclosure in target editor (Advanced section)

CSS Architecture:
- Define radius scale tokens (--radius-sm/md/lg/pill)
- Scope .cs-filter selectors to remove 7 !important overrides
- Consolidate duplicate toggle switch (filter-list → settings-toggle)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 18:46:38 +03:00
parent a4a0e39b9b
commit 304fa24389
47 changed files with 2594 additions and 250 deletions

View File

@@ -582,7 +582,7 @@ export function onSerialPortFocus() {
}
}
export function showAddDevice(presetType = null) {
export function showAddDevice(presetType = null, cloneData = null) {
// When no type specified: show type picker first
if (!presetType) {
showTypePicker({
@@ -623,6 +623,47 @@ export function showAddDevice(presetType = null) {
addDeviceModal.open();
onDeviceTypeChanged();
// Prefill fields from clone data (after onDeviceTypeChanged shows/hides fields)
if (cloneData) {
document.getElementById('device-name').value = (cloneData.name || '') + ' (Copy)';
// Clear URL — devices must have unique addresses, user must enter a new one
const urlInput = document.getElementById('device-url');
if (urlInput) urlInput.value = '';
// Prefill LED count
const ledCountInput = document.getElementById('device-led-count');
if (ledCountInput && cloneData.led_count) ledCountInput.value = cloneData.led_count;
// Prefill baud rate for serial devices
if (isSerialDevice(presetType)) {
const baudSelect = document.getElementById('device-baud-rate');
if (baudSelect && cloneData.baud_rate) baudSelect.value = String(cloneData.baud_rate);
}
// Prefill mock device fields
if (isMockDevice(presetType)) {
const ledTypeEl = document.getElementById('device-led-type');
if (ledTypeEl) ledTypeEl.value = cloneData.rgbw ? 'rgbw' : 'rgb';
const sendLatencyEl = document.getElementById('device-send-latency');
if (sendLatencyEl) sendLatencyEl.value = cloneData.send_latency_ms ?? 0;
}
// Prefill DMX fields
if (isDmxDevice(presetType)) {
const dmxProto = document.getElementById('device-dmx-protocol');
if (dmxProto && cloneData.dmx_protocol) dmxProto.value = cloneData.dmx_protocol;
const dmxUniverse = document.getElementById('device-dmx-start-universe');
if (dmxUniverse && cloneData.dmx_start_universe != null) dmxUniverse.value = cloneData.dmx_start_universe;
const dmxChannel = document.getElementById('device-dmx-start-channel');
if (dmxChannel && cloneData.dmx_start_channel != null) dmxChannel.value = cloneData.dmx_start_channel;
}
// Prefill CSPT template selector (after fetch completes)
if (cloneData.default_css_processing_template_id) {
csptCache.fetch().then(() => {
_ensureCsptEntitySelect();
const csptEl = document.getElementById('device-css-processing-template');
if (csptEl) csptEl.value = cloneData.default_css_processing_template_id;
});
}
}
setTimeout(() => {
desktopFocus(document.getElementById('device-name'));
addDeviceModal.snapshot();
@@ -984,3 +1025,18 @@ function _showGameSenseFields(show) {
const el = document.getElementById('device-gamesense-device-type-group');
if (el) el.style.display = show ? '' : 'none';
}
/* ── Clone device ──────────────────────────────────────────────── */
export async function cloneDevice(deviceId) {
try {
const resp = await fetchWithAuth(`/devices/${deviceId}`);
if (!resp.ok) throw new Error('Failed to load device');
const device = await resp.json();
showAddDevice(device.device_type || 'wled', device);
} catch (error) {
if (error.isAuth) return;
console.error('Failed to clone device:', error);
showToast(t('device.error.clone_failed'), 'error');
}
}