Frontend: structured error handling, state fixes, accessibility, i18n

- Enhance fetchWithAuth with auto-401, retry w/ exponential backoff, timeout
- Remove ~40 manual 401 checks across 10 feature files
- Fix state: brightness cache setter, manual edit flag resets, static import
- Add ARIA: role=dialog/tablist, aria-modal, aria-labelledby, aria-selected
- Add focus trapping in Modal base class, aria-expanded on hint toggles
- Fix WCAG AA color contrast with --primary-text-color variable
- Add i18n pluralization (CLDR rules for en/ru), getCurrentLocale export
- Replace hardcoded strings in dashboard.js and profiles.js
- Add data-i18n-aria-label support, 20 new keys in en.json and ru.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 01:18:29 +03:00
parent 2b90fafb9c
commit 3ae20761a1
41 changed files with 355 additions and 248 deletions

View File

@@ -1,11 +1,11 @@
<!-- Add Device Modal -->
<div id="add-device-modal" class="modal">
<div id="add-device-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="add-device-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="devices.add">Add New Device</h2>
<h2 id="add-device-modal-title" data-i18n="devices.add">Add New Device</h2>
<div class="modal-header-actions">
<button type="button" class="modal-header-btn" id="scan-network-btn" onclick="scanForDevices()" data-i18n-title="device.scan" title="Auto Discovery">&#x1F50D;</button>
<button class="modal-close-btn" onclick="closeAddDeviceModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeAddDeviceModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
</div>
<div class="modal-body">
@@ -82,8 +82,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeAddDeviceModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="document.getElementById('add-device-form').requestSubmit()" title="Add Device">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeAddDeviceModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="document.getElementById('add-device-form').requestSubmit()" title="Add Device" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Login Modal -->
<div id="api-key-modal" class="modal">
<div id="api-key-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="api-key-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="auth.title">🔑 Login to LED Grab</h2>
<button class="modal-close-btn" id="modal-close-x-btn" onclick="closeApiKeyModal()" title="Close">&#x2715;</button>
<h2 id="api-key-modal-title" data-i18n="auth.title">🔑 Login to LED Grab</h2>
<button class="modal-close-btn" id="modal-close-x-btn" onclick="closeApiKeyModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<form id="api-key-form" onsubmit="submitApiKey(event)">
<div class="modal-body">
@@ -32,8 +32,8 @@
<div id="api-key-error" class="error-message" style="display: none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-icon btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn" title="Cancel">&#x2715;</button>
<button type="submit" class="btn btn-icon btn-primary" title="Login">&#x2713;</button>
<button type="button" class="btn btn-icon btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button type="submit" class="btn btn-icon btn-primary" title="Login" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</form>
</div>

View File

@@ -1,10 +1,10 @@
<!-- Calibration Modal -->
<div id="calibration-modal" class="modal">
<div id="calibration-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="calibration-modal-title">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h2 data-i18n="calibration.title">📐 LED Calibration</h2>
<h2 id="calibration-modal-title" data-i18n="calibration.title">📐 LED Calibration</h2>
<button class="tutorial-trigger-btn" onclick="startCalibrationTutorial()" data-i18n-title="calibration.tutorial.start" title="Start tutorial">?</button>
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<input type="hidden" id="calibration-device-id">
@@ -137,8 +137,8 @@
<div id="calibration-error" class="error-message" style="display: none;"></div>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeCalibrationModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveCalibration()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeCalibrationModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveCalibration()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Template Modal -->
<div id="template-modal" class="modal">
<div id="template-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="template-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="template-modal-title" data-i18n="templates.add">Add Capture Template</h2>
<button class="modal-close-btn" onclick="closeTemplateModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeTemplateModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<input type="hidden" id="template-id">
@@ -38,8 +38,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeTemplateModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveTemplate()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeTemplateModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveTemplate()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Confirmation Modal -->
<div id="confirm-modal" class="modal">
<div id="confirm-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="confirm-modal-title">
<div class="modal-content" style="max-width: 450px;">
<div class="modal-header">
<h2 id="confirm-title">Confirm Action</h2>
<button class="modal-close-btn" onclick="closeConfirmModal(false)" title="Close">&#x2715;</button>
<h2 id="confirm-modal-title">Confirm Action</h2>
<button class="modal-close-btn" onclick="closeConfirmModal(false)" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<p id="confirm-message" class="modal-description"></p>

View File

@@ -1,9 +1,9 @@
<!-- General Settings Modal -->
<div id="device-settings-modal" class="modal">
<div id="device-settings-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="device-settings-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="settings.general.title">⚙️ General Settings</h2>
<button class="modal-close-btn" onclick="closeDeviceSettingsModal()" title="Close">&#x2715;</button>
<h2 id="device-settings-modal-title" data-i18n="settings.general.title">⚙️ General Settings</h2>
<button class="modal-close-btn" onclick="closeDeviceSettingsModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="device-settings-form">
@@ -83,8 +83,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeDeviceSettingsModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveDeviceSettings()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeDeviceSettingsModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveDeviceSettings()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Key Colors Editor Modal -->
<div id="kc-editor-modal" class="modal">
<div id="kc-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="kc-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="kc-editor-title" data-i18n="kc.add">🎨 Add Key Colors Target</h2>
<button class="modal-close-btn" onclick="closeKCEditorModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeKCEditorModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="kc-editor-form">
@@ -73,8 +73,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeKCEditorModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveKCEditor()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeKCEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveKCEditor()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Pattern Template Editor Modal -->
<div id="pattern-template-modal" class="modal">
<div id="pattern-template-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="pattern-template-modal-title">
<div class="modal-content modal-content-wide">
<div class="modal-header">
<h2 id="pattern-template-title" data-i18n="pattern.add">📄 Add Pattern Template</h2>
<button class="modal-close-btn" onclick="closePatternTemplateModal()" title="Close">&#x2715;</button>
<h2 id="pattern-template-modal-title" data-i18n="pattern.add">📄 Add Pattern Template</h2>
<button class="modal-close-btn" onclick="closePatternTemplateModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="pattern-template-form">
@@ -60,8 +60,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closePatternTemplateModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="savePatternTemplate()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closePatternTemplateModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="savePatternTemplate()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Processing Template Modal -->
<div id="pp-template-modal" class="modal">
<div id="pp-template-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="pp-template-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="pp-template-modal-title" data-i18n="postprocessing.add">Add Processing Template</h2>
<button class="modal-close-btn" onclick="closePPTemplateModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closePPTemplateModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<input type="hidden" id="pp-template-id">
@@ -33,8 +33,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closePPTemplateModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="savePPTemplate()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closePPTemplateModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="savePPTemplate()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Profile Editor Modal -->
<div id="profile-editor-modal" class="modal">
<div id="profile-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="profile-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="profile-editor-title" data-i18n="profiles.add">📋 Add Profile</h2>
<button class="modal-close-btn" onclick="closeProfileEditorModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeProfileEditorModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="profile-editor-form">
@@ -67,8 +67,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeProfileEditorModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveProfileEditor()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeProfileEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveProfileEditor()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Source Modal -->
<div id="stream-modal" class="modal">
<div id="stream-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="stream-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="stream-modal-title" data-i18n="streams.add">Add Source</h2>
<button class="modal-close-btn" onclick="closeStreamModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeStreamModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<input type="hidden" id="stream-id">
@@ -95,8 +95,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeStreamModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveStream()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeStreamModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveStream()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Target Editor Modal (name, device, source, settings) -->
<div id="target-editor-modal" class="modal">
<div id="target-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="target-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="target-editor-title" data-i18n="targets.add">🎯 Add Target</h2>
<button class="modal-close-btn" onclick="closeTargetEditorModal()" title="Close">&#x2715;</button>
<button class="modal-close-btn" onclick="closeTargetEditorModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="target-editor-form">
@@ -85,8 +85,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeTargetEditorModal()" title="Cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveTargetEditor()" title="Save">&#x2713;</button>
<button class="btn btn-icon btn-secondary" onclick="closeTargetEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveTargetEditor()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<!-- Test PP Template Modal -->
<div id="test-pp-template-modal" class="modal">
<div id="test-pp-template-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="test-pp-template-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="postprocessing.test.title">Test Processing Template</h2>
<button class="modal-close-btn" onclick="closeTestPPTemplateModal()" title="Close">&#x2715;</button>
<h2 id="test-pp-template-modal-title" data-i18n="postprocessing.test.title">Test Processing Template</h2>
<button class="modal-close-btn" onclick="closeTestPPTemplateModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<div class="form-group">

View File

@@ -1,9 +1,9 @@
<!-- Test Source Modal -->
<div id="test-stream-modal" class="modal">
<div id="test-stream-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="test-stream-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="streams.test.title">Test Source</h2>
<button class="modal-close-btn" onclick="closeTestStreamModal()" title="Close">&#x2715;</button>
<h2 id="test-stream-modal-title" data-i18n="streams.test.title">Test Source</h2>
<button class="modal-close-btn" onclick="closeTestStreamModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<div class="form-group">

View File

@@ -1,9 +1,9 @@
<!-- Test Template Modal -->
<div id="test-template-modal" class="modal">
<div id="test-template-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="test-template-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="templates.test.title">Test Capture Template</h2>
<button class="modal-close-btn" onclick="closeTestTemplateModal()" title="Close">&#x2715;</button>
<h2 id="test-template-modal-title" data-i18n="templates.test.title">Test Capture Template</h2>
<button class="modal-close-btn" onclick="closeTestTemplateModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<div class="form-group">