Add tags to all entity types with chip-based input and autocomplete

- Add `tags: List[str]` field to all 13 entity types (devices, output targets,
  CSS sources, picture sources, audio sources, value sources, sync clocks,
  automations, scene presets, capture/audio/PP/pattern templates)
- Update all stores, schemas, and route handlers for tag CRUD
- Add GET /api/v1/tags endpoint aggregating unique tags across all stores
- Create TagInput component with chip display, autocomplete dropdown,
  keyboard navigation, and API-backed suggestions
- Display tag chips on all entity cards (searchable via existing text filter)
- Add tag input to all 14 editor modals with dirty check support
- Add CSS styles and i18n keys (en/ru/zh) for tag UI
- Also includes code review fixes: thread safety, perf, store dedup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 22:20:19 +03:00
parent 2712c6682e
commit 30fa107ef7
120 changed files with 2471 additions and 1949 deletions

View File

@@ -85,6 +85,15 @@
<small class="input-hint" style="display:none" data-i18n="audio_source.description.hint">Optional notes about this audio source</small>
<input type="text" id="audio-source-description" data-i18n-placeholder="audio_source.description.placeholder" placeholder="Describe this audio source...">
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="audio-source-tags-container"></div>
</div>
</form>
</div>
<div class="modal-footer">

View File

@@ -34,6 +34,15 @@
<div id="audio-engine-config-fields"></div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="audio-template-tags-container"></div>
</div>
<div id="audio-template-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -85,6 +85,15 @@
<select id="automation-fallback-scene-id"></select>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="automation-tags-container"></div>
</div>
<div id="automation-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -34,6 +34,15 @@
<div id="engine-config-fields"></div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="capture-template-tags-container"></div>
</div>
<div id="template-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -576,6 +576,15 @@
</select>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="css-tags-container"></div>
</div>
<div id="css-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -136,6 +136,15 @@
</div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="device-tags-container"></div>
</div>
<div id="settings-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -80,6 +80,15 @@
<input type="range" id="kc-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('kc-editor-smoothing-value').textContent = this.value">
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="kc-tags-container"></div>
</div>
<div id="kc-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -56,6 +56,15 @@
<div id="pattern-rect-list" class="pattern-rect-list"></div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="pattern-tags-container"></div>
</div>
<div id="pattern-template-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -28,6 +28,15 @@
<input type="text" id="pp-template-description" data-i18n-placeholder="postprocessing.description_placeholder" placeholder="Describe this template...">
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="pp-template-tags-container"></div>
</div>
<div id="pp-template-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -37,6 +37,15 @@
<button type="button" id="scene-target-add-btn" class="btn btn-sm btn-secondary" onclick="addSceneTarget()" style="margin-top: 6px;">+ <span data-i18n="scenes.targets.add">Add Target</span></button>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="scene-tags-container"></div>
</div>
<div id="scene-preset-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -96,6 +96,15 @@
<input type="text" id="stream-description" data-i18n-placeholder="streams.description_placeholder" placeholder="Describe this source...">
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="stream-tags-container"></div>
</div>
<div id="stream-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -41,6 +41,15 @@
<small class="input-hint" style="display:none" data-i18n="sync_clock.description.hint">Optional notes about this clock's purpose</small>
<input type="text" id="sync-clock-description" data-i18n-placeholder="sync_clock.description.placeholder" placeholder="">
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="sync-clock-tags-container"></div>
</div>
</form>
</div>
<div class="modal-footer">

View File

@@ -113,6 +113,15 @@
</div>
</details>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="target-tags-container"></div>
</div>
<div id="target-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -267,6 +267,15 @@
<small class="input-hint" style="display:none" data-i18n="value_source.description.hint">Optional notes about this value source</small>
<input type="text" id="value-source-description" data-i18n-placeholder="value_source.description.placeholder" placeholder="Describe this value source...">
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="tags.label">Tags:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="tags.hint">Assign tags for grouping and filtering cards</small>
<div id="value-source-tags-container"></div>
</div>
</form>
</div>
<div class="modal-footer">