Fix automation badge overflow, dashboard crosslinks, compact numbers, icon grids, OpenRGB brightness
UI fixes: - Automation card badge moved to flex layout — title truncates, badge stays visible - Automation condition pills max-width increased to 280px - Dashboard crosslinks fixed: pass correct sub-tab key (led-targets not led) - navigateToCard only skips data load when tab already has cards in DOM - Badge gets white-space:nowrap + flex-shrink:0 to prevent wrapping New features: - formatCompact() for large frame/error counters (1.2M, 45.2K) with hover title - Log filter and log level selects replaced with IconSelect grids - OpenRGB devices now support software brightness control OpenRGB improvements: - Added brightness_control capability (uses software brightness fallback) - Change-threshold dedup compares raw pixels before brightness scaling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -288,16 +288,13 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
Builds raw OpenRGB UpdateZoneLeds packets directly with struct.pack,
|
Builds raw OpenRGB UpdateZoneLeds packets directly with struct.pack,
|
||||||
bypassing RGBColor object creation to avoid GC pressure.
|
bypassing RGBColor object creation to avoid GC pressure.
|
||||||
"""
|
"""
|
||||||
# Apply brightness scaling
|
|
||||||
if brightness < 255:
|
|
||||||
pixel_array = (pixel_array.astype(np.uint16) * brightness >> 8).astype(np.uint8)
|
|
||||||
|
|
||||||
# Truncate to match target LED count
|
# Truncate to match target LED count
|
||||||
n_target = self._device_led_count
|
n_target = self._device_led_count
|
||||||
if len(pixel_array) > n_target:
|
if len(pixel_array) > n_target:
|
||||||
pixel_array = pixel_array[:n_target]
|
pixel_array = pixel_array[:n_target]
|
||||||
|
|
||||||
# Change-threshold dedup — skip if average per-LED color change < 2
|
# Change-threshold dedup — compare RAW pixels before brightness scaling
|
||||||
|
# so low brightness doesn't crush differences below the threshold.
|
||||||
# GPU I2C/SMBus writes cause system-wide stalls; minimizing writes is critical.
|
# GPU I2C/SMBus writes cause system-wide stalls; minimizing writes is critical.
|
||||||
if self._last_sent_pixels is not None and self._last_sent_pixels.shape == pixel_array.shape:
|
if self._last_sent_pixels is not None and self._last_sent_pixels.shape == pixel_array.shape:
|
||||||
diff = np.mean(np.abs(pixel_array.astype(np.int16) - self._last_sent_pixels.astype(np.int16)))
|
diff = np.mean(np.abs(pixel_array.astype(np.int16) - self._last_sent_pixels.astype(np.int16)))
|
||||||
@@ -305,6 +302,10 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
return
|
return
|
||||||
self._last_sent_pixels = pixel_array.copy()
|
self._last_sent_pixels = pixel_array.copy()
|
||||||
|
|
||||||
|
# Apply brightness scaling after dedup
|
||||||
|
if brightness < 255:
|
||||||
|
pixel_array = (pixel_array.astype(np.uint16) * brightness >> 8).astype(np.uint8)
|
||||||
|
|
||||||
# Separate mode: resample full pixel array independently per zone
|
# Separate mode: resample full pixel array independently per zone
|
||||||
if self._zone_mode == "separate" and len(self._target_zones) > 1:
|
if self._zone_mode == "separate" and len(self._target_zones) > 1:
|
||||||
n_src = len(pixel_array)
|
n_src = len(pixel_array)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class OpenRGBDeviceProvider(LEDDeviceProvider):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def capabilities(self) -> set:
|
def capabilities(self) -> set:
|
||||||
return {"health_check", "auto_restore", "static_color"}
|
return {"health_check", "auto_restore", "static_color", "brightness_control"}
|
||||||
|
|
||||||
def create_client(self, url: str, **kwargs) -> LEDClient:
|
def create_client(self, url: str, **kwargs) -> LEDClient:
|
||||||
return OpenRGBLEDClient(
|
return OpenRGBLEDClient(
|
||||||
|
|||||||
@@ -27,6 +27,11 @@
|
|||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Automation condition pills need more room than the default 180px */
|
||||||
|
[data-automation-id] .stream-card-prop {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Automation condition editor rows */
|
/* Automation condition editor rows */
|
||||||
.automation-condition-row {
|
.automation-condition-row {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|||||||
@@ -419,6 +419,16 @@ body.cs-drag-active .card-drag-handle {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title > .icon {
|
.card-title > .icon {
|
||||||
|
|||||||
@@ -95,6 +95,8 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-description {
|
.template-description {
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ export function navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue) {
|
|||||||
// Push current location to history so browser back returns here
|
// Push current location to history so browser back returns here
|
||||||
history.pushState(null, '', location.hash || '#');
|
history.pushState(null, '', location.hash || '#');
|
||||||
|
|
||||||
// Activate tab visually without triggering a data reload —
|
// Switch to the target tab, allowing data load if needed.
|
||||||
// the command palette already fetched fresh data, and a reload
|
// skipLoad only when tab data is already in the DOM (cards exist).
|
||||||
// would re-render all cards, destroying the highlight.
|
const tabPanel = document.getElementById(`tab-${tab}`);
|
||||||
switchTab(tab, { skipLoad: true });
|
const hasCards = tabPanel && tabPanel.querySelector('.card');
|
||||||
|
switchTab(tab, { skipLoad: !!hasCards });
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (subTab) {
|
if (subTab) {
|
||||||
|
|||||||
@@ -362,6 +362,22 @@ export function setTabRefreshing(containerId, refreshing) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Format a large number compactly: 999 → "999", 1200 → "1.2K", 2500000 → "2.5M" */
|
||||||
|
export function formatCompact(n) {
|
||||||
|
if (n == null || n < 0) return '-';
|
||||||
|
if (n < 1000) return String(n);
|
||||||
|
if (n < 1_000_000) {
|
||||||
|
const v = n / 1000;
|
||||||
|
return (v < 10 ? v.toFixed(1) : Math.round(v)) + 'K';
|
||||||
|
}
|
||||||
|
if (n < 1_000_000_000) {
|
||||||
|
const v = n / 1_000_000;
|
||||||
|
return (v < 10 ? v.toFixed(1) : Math.round(v)) + 'M';
|
||||||
|
}
|
||||||
|
const v = n / 1_000_000_000;
|
||||||
|
return (v < 10 ? v.toFixed(1) : Math.round(v)) + 'B';
|
||||||
|
}
|
||||||
|
|
||||||
export function formatUptime(seconds) {
|
export function formatUptime(seconds) {
|
||||||
if (!seconds || seconds <= 0) return '-';
|
if (!seconds || seconds <= 0) return '-';
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
|||||||
content: `
|
content: `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title" title="${escapeHtml(automation.name)}">
|
<div class="card-title" title="${escapeHtml(automation.name)}">
|
||||||
${escapeHtml(automation.name)}
|
<span class="card-title-text">${escapeHtml(automation.name)}</span>
|
||||||
<span class="badge badge-automation-${statusClass}">${statusText}</span>
|
<span class="badge badge-automation-${statusClass}">${statusText}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { apiKey, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval, colorStripSourcesCache, devicesCache, outputTargetsCache } from '../core/state.js';
|
import { apiKey, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval, colorStripSourcesCache, devicesCache, outputTargetsCache } from '../core/state.js';
|
||||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm, formatUptime, setTabRefreshing } from '../core/ui.js';
|
import { showToast, showConfirm, formatUptime, formatCompact, setTabRefreshing } from '../core/ui.js';
|
||||||
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
|
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
|
||||||
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
||||||
import {
|
import {
|
||||||
@@ -189,7 +189,7 @@ function _updateRunningMetrics(enrichedRunning) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
||||||
if (errorsEl) errorsEl.innerHTML = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`;
|
if (errorsEl) { errorsEl.innerHTML = `${errors > 0 ? ICON_WARNING : ICON_OK} ${formatCompact(errors)}`; errorsEl.title = String(errors); }
|
||||||
|
|
||||||
// Update health dot — prefer streaming reachability when processing
|
// Update health dot — prefer streaming reachability when processing
|
||||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||||
@@ -542,7 +542,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||||
const icon = ICON_TARGET;
|
const icon = ICON_TARGET;
|
||||||
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
|
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
|
||||||
const navSubTab = isLed ? 'led' : 'key_colors';
|
const navSubTab = isLed ? 'led-targets' : 'kc-targets';
|
||||||
const navSection = isLed ? 'led-targets' : 'kc-targets';
|
const navSection = isLed ? 'led-targets' : 'kc-targets';
|
||||||
const navAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
|
const navAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
|
||||||
const navOnclick = `if(!event.target.closest('button')){navigateToCard('targets','${navSubTab}','${navSection}','${navAttr}','${target.id}')}`;
|
const navOnclick = `if(!event.target.closest('button')){navigateToCard('targets','${navSubTab}','${navSection}','${navAttr}','${target.id}')}`;
|
||||||
@@ -607,7 +607,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
<div class="dashboard-metric-value" data-uptime-text="${target.id}">${ICON_CLOCK} ${uptime}</div>
|
<div class="dashboard-metric-value" data-uptime-text="${target.id}">${ICON_CLOCK} ${uptime}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-metric" title="${t('dashboard.errors')}">
|
<div class="dashboard-metric" title="${t('dashboard.errors')}">
|
||||||
<div class="dashboard-metric-value" data-errors-text="${target.id}">${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}</div>
|
<div class="dashboard-metric-value" data-errors-text="${target.id}" title="${errors}">${errors > 0 ? ICON_WARNING : ICON_OK} ${formatCompact(errors)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-actions">
|
<div class="dashboard-target-actions">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from '../core/state.js';
|
} from '../core/state.js';
|
||||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { lockBody, showToast, showConfirm, formatUptime, desktopFocus } from '../core/ui.js';
|
import { lockBody, showToast, showConfirm, formatUptime, formatCompact, desktopFocus } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import {
|
import {
|
||||||
getValueSourceIcon, getPictureSourceIcon,
|
getValueSourceIcon, getPictureSourceIcon,
|
||||||
@@ -153,13 +153,13 @@ export function patchKCTargetMetrics(target) {
|
|||||||
if (fpsTarget) fpsTarget.textContent = state.fps_target || 0;
|
if (fpsTarget) fpsTarget.textContent = state.fps_target || 0;
|
||||||
|
|
||||||
const frames = card.querySelector('[data-tm="frames"]');
|
const frames = card.querySelector('[data-tm="frames"]');
|
||||||
if (frames) frames.textContent = metrics.frames_processed || 0;
|
if (frames) { frames.textContent = formatCompact(metrics.frames_processed || 0); frames.title = String(metrics.frames_processed || 0); }
|
||||||
|
|
||||||
const keepalive = card.querySelector('[data-tm="keepalive"]');
|
const keepalive = card.querySelector('[data-tm="keepalive"]');
|
||||||
if (keepalive) keepalive.textContent = state.frames_keepalive ?? '-';
|
if (keepalive) { keepalive.textContent = formatCompact(state.frames_keepalive ?? 0); keepalive.title = String(state.frames_keepalive ?? 0); }
|
||||||
|
|
||||||
const errors = card.querySelector('[data-tm="errors"]');
|
const errors = card.querySelector('[data-tm="errors"]');
|
||||||
if (errors) errors.textContent = metrics.errors_count || 0;
|
if (errors) { errors.textContent = formatCompact(metrics.errors_count || 0); errors.title = String(metrics.errors_count || 0); }
|
||||||
|
|
||||||
const uptime = card.querySelector('[data-tm="uptime"]');
|
const uptime = card.querySelector('[data-tm="uptime"]');
|
||||||
if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
|
if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Modal } from '../core/modal.js';
|
|||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { ICON_UNDO, ICON_DOWNLOAD } from '../core/icons.js';
|
import { ICON_UNDO, ICON_DOWNLOAD } from '../core/icons.js';
|
||||||
|
import { IconSelect } from '../core/icon-select.js';
|
||||||
|
|
||||||
// ─── Log Viewer ────────────────────────────────────────────
|
// ─── Log Viewer ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -130,9 +131,53 @@ export function applyLogFilter() {
|
|||||||
// Simple modal (no form / no dirty check needed)
|
// Simple modal (no form / no dirty check needed)
|
||||||
const settingsModal = new Modal('settings-modal');
|
const settingsModal = new Modal('settings-modal');
|
||||||
|
|
||||||
|
let _logFilterIconSelect = null;
|
||||||
|
let _logLevelIconSelect = null;
|
||||||
|
|
||||||
|
const _LOG_LEVEL_ITEMS = [
|
||||||
|
{ value: 'DEBUG', icon: '<span style="color:#9e9e9e;font-weight:700">D</span>', label: 'DEBUG', desc: t('settings.log_level.desc.debug') },
|
||||||
|
{ value: 'INFO', icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: 'INFO', desc: t('settings.log_level.desc.info') },
|
||||||
|
{ value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: 'WARNING', desc: t('settings.log_level.desc.warning') },
|
||||||
|
{ value: 'ERROR', icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: 'ERROR', desc: t('settings.log_level.desc.error') },
|
||||||
|
{ value: 'CRITICAL', icon: '<span style="color:#ff1744;font-weight:700">!</span>', label: 'CRITICAL', desc: t('settings.log_level.desc.critical') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const _LOG_FILTER_ITEMS = [
|
||||||
|
{ value: 'all', icon: '<span style="color:#9e9e9e;font-weight:700">*</span>', label: t('settings.logs.filter.all'), desc: t('settings.logs.filter.all_desc') },
|
||||||
|
{ value: 'INFO', icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: t('settings.logs.filter.info'), desc: t('settings.logs.filter.info_desc') },
|
||||||
|
{ value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: t('settings.logs.filter.warning'), desc: t('settings.logs.filter.warning_desc') },
|
||||||
|
{ value: 'ERROR', icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: t('settings.logs.filter.error'), desc: t('settings.logs.filter.error_desc') },
|
||||||
|
];
|
||||||
|
|
||||||
export function openSettingsModal() {
|
export function openSettingsModal() {
|
||||||
document.getElementById('settings-error').style.display = 'none';
|
document.getElementById('settings-error').style.display = 'none';
|
||||||
settingsModal.open();
|
settingsModal.open();
|
||||||
|
|
||||||
|
// Initialize log filter icon select
|
||||||
|
if (!_logFilterIconSelect) {
|
||||||
|
const filterSel = document.getElementById('log-viewer-filter');
|
||||||
|
if (filterSel) {
|
||||||
|
_logFilterIconSelect = new IconSelect({
|
||||||
|
target: filterSel,
|
||||||
|
items: _LOG_FILTER_ITEMS,
|
||||||
|
columns: 2,
|
||||||
|
onChange: () => applyLogFilter(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initialize log level icon select
|
||||||
|
if (!_logLevelIconSelect) {
|
||||||
|
const levelSel = document.getElementById('settings-log-level');
|
||||||
|
if (levelSel) {
|
||||||
|
_logLevelIconSelect = new IconSelect({
|
||||||
|
target: levelSel,
|
||||||
|
items: _LOG_LEVEL_ITEMS,
|
||||||
|
columns: 3,
|
||||||
|
onChange: () => setLogLevel(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadApiKeysList();
|
loadApiKeysList();
|
||||||
loadAutoBackupSettings();
|
loadAutoBackupSettings();
|
||||||
loadBackupList();
|
loadBackupList();
|
||||||
@@ -559,8 +604,12 @@ export async function loadLogLevel() {
|
|||||||
const resp = await fetchWithAuth('/system/log-level');
|
const resp = await fetchWithAuth('/system/log-level');
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
if (_logLevelIconSelect) {
|
||||||
|
_logLevelIconSelect.setValue(data.level);
|
||||||
|
} else {
|
||||||
const select = document.getElementById('settings-log-level');
|
const select = document.getElementById('settings-log-level');
|
||||||
if (select) select.value = data.level;
|
if (select) select.value = data.level;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load log level:', err);
|
console.error('Failed to load log level:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '../core/state.js';
|
} from '../core/state.js';
|
||||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isOpenrgbDevice } from '../core/api.js';
|
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isOpenrgbDevice } from '../core/api.js';
|
||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm, formatUptime, setTabRefreshing, desktopFocus } from '../core/ui.js';
|
import { showToast, showConfirm, formatUptime, formatCompact, setTabRefreshing, desktopFocus } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, enrichOpenrgbZoneBadges, _computeMaxFps, getZoneCountCache } from './devices.js';
|
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, enrichOpenrgbZoneBadges, _computeMaxFps, getZoneCountCache } from './devices.js';
|
||||||
import { _splitOpenrgbZone } from './device-discovery.js';
|
import { _splitOpenrgbZone } from './device-discovery.js';
|
||||||
@@ -890,13 +890,13 @@ function _patchTargetMetrics(target) {
|
|||||||
if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
|
if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
|
||||||
|
|
||||||
const frames = card.querySelector('[data-tm="frames"]');
|
const frames = card.querySelector('[data-tm="frames"]');
|
||||||
if (frames) frames.textContent = metrics.frames_processed || 0;
|
if (frames) { frames.textContent = formatCompact(metrics.frames_processed || 0); frames.title = String(metrics.frames_processed || 0); }
|
||||||
|
|
||||||
const keepalive = card.querySelector('[data-tm="keepalive"]');
|
const keepalive = card.querySelector('[data-tm="keepalive"]');
|
||||||
if (keepalive) keepalive.textContent = state.frames_keepalive ?? '-';
|
if (keepalive) { keepalive.textContent = formatCompact(state.frames_keepalive ?? 0); keepalive.title = String(state.frames_keepalive ?? 0); }
|
||||||
|
|
||||||
const errors = card.querySelector('[data-tm="errors"]');
|
const errors = card.querySelector('[data-tm="errors"]');
|
||||||
if (errors) errors.textContent = metrics.errors_count || 0;
|
if (errors) { errors.textContent = formatCompact(metrics.errors_count || 0); errors.title = String(metrics.errors_count || 0); }
|
||||||
|
|
||||||
// Error indicator near target name
|
// Error indicator near target name
|
||||||
const errorIndicator = card.querySelector('.target-error-indicator');
|
const errorIndicator = card.querySelector('.target-error-indicator');
|
||||||
|
|||||||
@@ -1429,6 +1429,11 @@
|
|||||||
"settings.log_level.save": "Apply",
|
"settings.log_level.save": "Apply",
|
||||||
"settings.log_level.saved": "Log level changed",
|
"settings.log_level.saved": "Log level changed",
|
||||||
"settings.log_level.save_error": "Failed to change log level",
|
"settings.log_level.save_error": "Failed to change log level",
|
||||||
|
"settings.log_level.desc.debug": "Verbose developer output",
|
||||||
|
"settings.log_level.desc.info": "Normal operation messages",
|
||||||
|
"settings.log_level.desc.warning": "Potential problems",
|
||||||
|
"settings.log_level.desc.error": "Failures only",
|
||||||
|
"settings.log_level.desc.critical": "Fatal errors only",
|
||||||
"settings.auto_backup.label": "Auto-Backup",
|
"settings.auto_backup.label": "Auto-Backup",
|
||||||
"settings.auto_backup.hint": "Automatically create periodic backups of all configuration. Old backups are pruned when the maximum count is reached.",
|
"settings.auto_backup.hint": "Automatically create periodic backups of all configuration. Old backups are pruned when the maximum count is reached.",
|
||||||
"settings.auto_backup.enable": "Enable auto-backup",
|
"settings.auto_backup.enable": "Enable auto-backup",
|
||||||
@@ -1473,6 +1478,10 @@
|
|||||||
"settings.logs.filter.info": "Info+",
|
"settings.logs.filter.info": "Info+",
|
||||||
"settings.logs.filter.warning": "Warning+",
|
"settings.logs.filter.warning": "Warning+",
|
||||||
"settings.logs.filter.error": "Error only",
|
"settings.logs.filter.error": "Error only",
|
||||||
|
"settings.logs.filter.all_desc": "Show all log messages",
|
||||||
|
"settings.logs.filter.info_desc": "Info, warning, and errors",
|
||||||
|
"settings.logs.filter.warning_desc": "Warnings and errors only",
|
||||||
|
"settings.logs.filter.error_desc": "Errors only",
|
||||||
"device.error.power_off_failed": "Failed to turn off device",
|
"device.error.power_off_failed": "Failed to turn off device",
|
||||||
"device.removed": "Device removed",
|
"device.removed": "Device removed",
|
||||||
"device.error.remove_failed": "Failed to remove device",
|
"device.error.remove_failed": "Failed to remove device",
|
||||||
|
|||||||
@@ -1429,6 +1429,11 @@
|
|||||||
"settings.log_level.save": "Применить",
|
"settings.log_level.save": "Применить",
|
||||||
"settings.log_level.saved": "Уровень логирования изменён",
|
"settings.log_level.saved": "Уровень логирования изменён",
|
||||||
"settings.log_level.save_error": "Не удалось изменить уровень логирования",
|
"settings.log_level.save_error": "Не удалось изменить уровень логирования",
|
||||||
|
"settings.log_level.desc.debug": "Подробный вывод для разработки",
|
||||||
|
"settings.log_level.desc.info": "Обычные сообщения",
|
||||||
|
"settings.log_level.desc.warning": "Возможные проблемы",
|
||||||
|
"settings.log_level.desc.error": "Только ошибки",
|
||||||
|
"settings.log_level.desc.critical": "Только критические ошибки",
|
||||||
"settings.auto_backup.label": "Авто-бэкап",
|
"settings.auto_backup.label": "Авто-бэкап",
|
||||||
"settings.auto_backup.hint": "Автоматическое создание периодических резервных копий конфигурации. Старые копии удаляются при превышении максимального количества.",
|
"settings.auto_backup.hint": "Автоматическое создание периодических резервных копий конфигурации. Старые копии удаляются при превышении максимального количества.",
|
||||||
"settings.auto_backup.enable": "Включить авто-бэкап",
|
"settings.auto_backup.enable": "Включить авто-бэкап",
|
||||||
@@ -1473,6 +1478,10 @@
|
|||||||
"settings.logs.filter.info": "Info+",
|
"settings.logs.filter.info": "Info+",
|
||||||
"settings.logs.filter.warning": "Warning+",
|
"settings.logs.filter.warning": "Warning+",
|
||||||
"settings.logs.filter.error": "Только ошибки",
|
"settings.logs.filter.error": "Только ошибки",
|
||||||
|
"settings.logs.filter.all_desc": "Все сообщения лога",
|
||||||
|
"settings.logs.filter.info_desc": "Info, предупреждения и ошибки",
|
||||||
|
"settings.logs.filter.warning_desc": "Только предупреждения и ошибки",
|
||||||
|
"settings.logs.filter.error_desc": "Только ошибки",
|
||||||
"device.error.power_off_failed": "Не удалось выключить устройство",
|
"device.error.power_off_failed": "Не удалось выключить устройство",
|
||||||
"device.removed": "Устройство удалено",
|
"device.removed": "Устройство удалено",
|
||||||
"device.error.remove_failed": "Не удалось удалить устройство",
|
"device.error.remove_failed": "Не удалось удалить устройство",
|
||||||
|
|||||||
@@ -1429,6 +1429,11 @@
|
|||||||
"settings.log_level.save": "应用",
|
"settings.log_level.save": "应用",
|
||||||
"settings.log_level.saved": "日志级别已更改",
|
"settings.log_level.saved": "日志级别已更改",
|
||||||
"settings.log_level.save_error": "更改日志级别失败",
|
"settings.log_level.save_error": "更改日志级别失败",
|
||||||
|
"settings.log_level.desc.debug": "详细开发输出",
|
||||||
|
"settings.log_level.desc.info": "正常运行消息",
|
||||||
|
"settings.log_level.desc.warning": "潜在问题",
|
||||||
|
"settings.log_level.desc.error": "仅显示错误",
|
||||||
|
"settings.log_level.desc.critical": "仅显示致命错误",
|
||||||
"settings.auto_backup.label": "自动备份",
|
"settings.auto_backup.label": "自动备份",
|
||||||
"settings.auto_backup.hint": "自动定期创建所有配置的备份。当达到最大数量时,旧备份会被自动清理。",
|
"settings.auto_backup.hint": "自动定期创建所有配置的备份。当达到最大数量时,旧备份会被自动清理。",
|
||||||
"settings.auto_backup.enable": "启用自动备份",
|
"settings.auto_backup.enable": "启用自动备份",
|
||||||
@@ -1473,6 +1478,10 @@
|
|||||||
"settings.logs.filter.info": "Info+",
|
"settings.logs.filter.info": "Info+",
|
||||||
"settings.logs.filter.warning": "Warning+",
|
"settings.logs.filter.warning": "Warning+",
|
||||||
"settings.logs.filter.error": "仅错误",
|
"settings.logs.filter.error": "仅错误",
|
||||||
|
"settings.logs.filter.all_desc": "显示所有日志消息",
|
||||||
|
"settings.logs.filter.info_desc": "Info、警告和错误",
|
||||||
|
"settings.logs.filter.warning_desc": "仅警告和错误",
|
||||||
|
"settings.logs.filter.error_desc": "仅错误",
|
||||||
"device.error.power_off_failed": "关闭设备失败",
|
"device.error.power_off_failed": "关闭设备失败",
|
||||||
"device.removed": "设备已移除",
|
"device.removed": "设备已移除",
|
||||||
"device.error.remove_failed": "移除设备失败",
|
"device.error.remove_failed": "移除设备失败",
|
||||||
|
|||||||
@@ -199,16 +199,13 @@
|
|||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="input-hint" style="display:none" data-i18n="settings.log_level.hint">Change the server log verbosity at runtime. DEBUG shows the most detail; CRITICAL shows only fatal errors.</small>
|
<small class="input-hint" style="display:none" data-i18n="settings.log_level.hint">Change the server log verbosity at runtime. DEBUG shows the most detail; CRITICAL shows only fatal errors.</small>
|
||||||
<div style="display:flex;gap:0.5rem;">
|
<select id="settings-log-level">
|
||||||
<select id="settings-log-level" style="flex:1">
|
|
||||||
<option value="DEBUG">DEBUG</option>
|
<option value="DEBUG">DEBUG</option>
|
||||||
<option value="INFO">INFO</option>
|
<option value="INFO">INFO</option>
|
||||||
<option value="WARNING">WARNING</option>
|
<option value="WARNING">WARNING</option>
|
||||||
<option value="ERROR">ERROR</option>
|
<option value="ERROR">ERROR</option>
|
||||||
<option value="CRITICAL">CRITICAL</option>
|
<option value="CRITICAL">CRITICAL</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-primary" onclick="setLogLevel()" data-i18n="settings.log_level.save">Apply</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Restart section -->
|
<!-- Restart section -->
|
||||||
|
|||||||
Reference in New Issue
Block a user