Add stop-all buttons to target sections, perf chart color reset, and TODO

- Add stop-all buttons to LED targets and KC targets section headers
  (visible only when targets are running, uses headerExtra on CardSection)
- Add reset ability to performance chart color pickers (removes custom
  color from localStorage and reverts to default)
- Remove CODEBASE_REVIEW.md
- Add prioritized TODO.md with P1/P2/P3 feature roadmap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 01:46:58 +03:00
parent 90acae5207
commit 62b3d44e63
10 changed files with 125 additions and 63 deletions

View File

@@ -97,6 +97,7 @@ import {
loadTargetsTab, switchTargetSubTab,
showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
startTargetProcessing, stopTargetProcessing,
stopAllLedTargets, stopAllKCTargets,
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview, toggleTargetAutoStart,
expandAllTargetSections, collapseAllTargetSections,
@@ -343,6 +344,8 @@ Object.assign(window, {
saveTargetEditor,
startTargetProcessing,
stopTargetProcessing,
stopAllLedTargets,
stopAllKCTargets,
startTargetOverlay,
stopTargetOverlay,
deleteTarget,

View File

@@ -43,13 +43,15 @@ export class CardSection {
* @param {string} opts.gridClass CSS class for the card grid: 'devices-grid' | 'templates-grid'
* @param {string} [opts.addCardOnclick] onclick handler string for the "+" add card
* @param {string} [opts.keyAttr] data attribute that uniquely identifies cards (e.g. 'data-device-id')
* @param {string} [opts.headerExtra] Extra HTML injected between count badge and filter (e.g. action buttons)
*/
constructor(sectionKey, { titleKey, gridClass, addCardOnclick, keyAttr }) {
constructor(sectionKey, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra }) {
this.sectionKey = sectionKey;
this.titleKey = titleKey;
this.gridClass = gridClass;
this.addCardOnclick = addCardOnclick || '';
this.keyAttr = keyAttr || '';
this.headerExtra = headerExtra || '';
this._filterValue = '';
this._lastItems = null;
this._dragState = null;
@@ -86,6 +88,7 @@ export class CardSection {
<span class="cs-chevron"${chevronStyle}>&#9654;</span>
<span class="cs-title">${t(this.titleKey)}</span>
<span class="cs-count">${count}</span>
${this.headerExtra ? `<span class="cs-header-extra">${this.headerExtra}</span>` : ''}
<div class="cs-filter-wrap">
<input type="text" class="cs-filter" data-cs-filter="${this.sectionKey}"
placeholder="${t('section.filter.placeholder')}" autocomplete="off">
@@ -107,7 +110,7 @@ export class CardSection {
if (!header || !content) return;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.cs-filter-wrap')) return;
if (e.target.closest('.cs-filter-wrap') || e.target.closest('.cs-header-extra')) return;
this._toggleCollapse(header, content);
});

View File

@@ -23,7 +23,16 @@ function _getColor(key) {
}
function _onChartColorChange(key, hex) {
localStorage.setItem(`perfChartColor_${key}`, hex);
if (hex) {
localStorage.setItem(`perfChartColor_${key}`, hex);
} else {
// Reset: remove saved color, fall back to default
localStorage.removeItem(`perfChartColor_${key}`);
hex = _getColor(key);
// Update swatch to show the actual default color
const swatch = document.getElementById(`cp-swatch-perf-${key}`);
if (swatch) swatch.style.background = hex;
}
const chart = _charts[key];
if (chart) {
chart.data.datasets[0].borderColor = hex;
@@ -42,21 +51,21 @@ export function renderPerfSection() {
return `<div class="perf-charts-grid">
<div class="perf-chart-card">
<div class="perf-chart-header">
<span class="perf-chart-label">${t('dashboard.perf.cpu')} ${createColorPicker({ id: 'perf-cpu', currentColor: _getColor('cpu'), anchor: 'left' })}</span>
<span class="perf-chart-label">${t('dashboard.perf.cpu')} ${createColorPicker({ id: 'perf-cpu', currentColor: _getColor('cpu'), anchor: 'left', showReset: true })}</span>
<span class="perf-chart-value" id="perf-cpu-value">-</span>
</div>
<div class="perf-chart-wrap"><span class="perf-chart-subtitle" id="perf-cpu-name"></span><canvas id="perf-chart-cpu"></canvas></div>
</div>
<div class="perf-chart-card">
<div class="perf-chart-header">
<span class="perf-chart-label">${t('dashboard.perf.ram')} ${createColorPicker({ id: 'perf-ram', currentColor: _getColor('ram'), anchor: 'left' })}</span>
<span class="perf-chart-label">${t('dashboard.perf.ram')} ${createColorPicker({ id: 'perf-ram', currentColor: _getColor('ram'), anchor: 'left', showReset: true })}</span>
<span class="perf-chart-value" id="perf-ram-value">-</span>
</div>
<div class="perf-chart-wrap"><canvas id="perf-chart-ram"></canvas></div>
</div>
<div class="perf-chart-card" id="perf-gpu-card">
<div class="perf-chart-header">
<span class="perf-chart-label">${t('dashboard.perf.gpu')} ${createColorPicker({ id: 'perf-gpu', currentColor: _getColor('gpu'), anchor: 'left' })}</span>
<span class="perf-chart-label">${t('dashboard.perf.gpu')} ${createColorPicker({ id: 'perf-gpu', currentColor: _getColor('gpu'), anchor: 'left', showReset: true })}</span>
<span class="perf-chart-value" id="perf-gpu-value">-</span>
</div>
<div class="perf-chart-wrap"><span class="perf-chart-subtitle" id="perf-gpu-name"></span><canvas id="perf-chart-gpu"></canvas></div>

View File

@@ -33,8 +33,8 @@ import { updateSubTabHash, updateTabBadge } from './tabs.js';
// ── Card section instances ──
const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()", keyAttr: 'data-device-id' });
const csColorStrips = new CardSection('led-css', { titleKey: 'targets.section.color_strips', gridClass: 'devices-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id' });
const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id' });
const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id' });
const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllLedTargets()" data-stop-all="led">${ICON_STOP}</button>` });
const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllKCTargets()" data-stop-all="kc">${ICON_STOP}</button>` });
const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()", keyAttr: 'data-pattern-template-id' });
// Re-render targets tab when language changes (only if tab is active)
@@ -621,6 +621,14 @@ export async function loadTargetsTab() {
CardSection.bindAll([csDevices, csColorStrips, csLedTargets, csKCTargets, csPatternTemplates]);
}
// Show/hide stop-all buttons based on running state
const ledRunning = ledTargets.some(t => t.state && t.state.processing);
const kcRunning = kcTargets.some(t => t.state && t.state.processing);
const ledStopBtn = container.querySelector('[data-stop-all="led"]');
const kcStopBtn = container.querySelector('[data-stop-all="kc"]');
if (ledStopBtn) ledStopBtn.style.display = ledRunning ? '' : 'none';
if (kcStopBtn) kcStopBtn.style.display = kcRunning ? '' : 'none';
// Patch volatile metrics in-place (avoids full card replacement on polls)
for (const tgt of ledTargets) {
if (tgt.state && tgt.state.processing) _patchTargetMetrics(tgt);
@@ -983,6 +991,40 @@ export async function stopTargetProcessing(targetId) {
});
}
export async function stopAllLedTargets() {
await _stopAllByType('led');
}
export async function stopAllKCTargets() {
await _stopAllByType('key_colors');
}
async function _stopAllByType(targetType) {
try {
const [targetsResp, statesResp] = await Promise.all([
fetchWithAuth('/picture-targets'),
fetchWithAuth('/picture-targets/batch/states'),
]);
const data = await targetsResp.json();
const statesData = statesResp.ok ? await statesResp.json() : { states: {} };
const states = statesData.states || {};
const typeMatch = targetType === 'led' ? t => t.target_type === 'led' || t.target_type === 'wled' : t => t.target_type === targetType;
const running = (data.targets || []).filter(t => typeMatch(t) && states[t.id]?.processing);
if (!running.length) {
showToast(t('targets.stop_all.none_running'), 'info');
return;
}
await Promise.all(running.map(t =>
fetchWithAuth(`/picture-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {})
));
showToast(t('targets.stop_all.stopped', { count: running.length }), 'success');
loadTargetsTab();
} catch (error) {
if (error.isAuth) return;
showToast(t('targets.stop_all.error'), 'error');
}
}
export async function startTargetOverlay(targetId) {
await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/start`, {