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:
@@ -682,10 +682,22 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cs-collapsed .cs-filter-wrap {
|
||||
.cs-collapsed .cs-filter-wrap,
|
||||
.cs-collapsed .cs-header-extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cs-header-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cs-header-extra .btn {
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cs-filter-wrap {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}>▶</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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -1093,6 +1093,9 @@
|
||||
"target.error.clone_failed": "Failed to clone target",
|
||||
"target.error.autostart_toggle_failed": "Failed to toggle auto-start",
|
||||
"target.error.delete_failed": "Failed to delete target",
|
||||
"targets.stop_all.none_running": "No targets are currently running",
|
||||
"targets.stop_all.stopped": "Stopped {count} target(s)",
|
||||
"targets.stop_all.error": "Failed to stop targets",
|
||||
"audio_source.error.load": "Failed to load audio source",
|
||||
"audio_template.error.clone_failed": "Failed to clone audio template",
|
||||
"value_source.error.load": "Failed to load value source",
|
||||
|
||||
@@ -1093,6 +1093,9 @@
|
||||
"target.error.clone_failed": "Не удалось клонировать цель",
|
||||
"target.error.autostart_toggle_failed": "Не удалось переключить автозапуск",
|
||||
"target.error.delete_failed": "Не удалось удалить цель",
|
||||
"targets.stop_all.none_running": "Нет запущенных целей",
|
||||
"targets.stop_all.stopped": "Остановлено целей: {count}",
|
||||
"targets.stop_all.error": "Не удалось остановить цели",
|
||||
"audio_source.error.load": "Не удалось загрузить аудиоисточник",
|
||||
"audio_template.error.clone_failed": "Не удалось клонировать аудиошаблон",
|
||||
"value_source.error.load": "Не удалось загрузить источник значений",
|
||||
|
||||
@@ -1093,6 +1093,9 @@
|
||||
"target.error.clone_failed": "克隆目标失败",
|
||||
"target.error.autostart_toggle_failed": "切换自动启动失败",
|
||||
"target.error.delete_failed": "删除目标失败",
|
||||
"targets.stop_all.none_running": "当前没有运行中的目标",
|
||||
"targets.stop_all.stopped": "已停止 {count} 个目标",
|
||||
"targets.stop_all.error": "停止目标失败",
|
||||
"audio_source.error.load": "加载音频源失败",
|
||||
"audio_template.error.clone_failed": "克隆音频模板失败",
|
||||
"value_source.error.load": "加载数值源失败",
|
||||
|
||||
Reference in New Issue
Block a user