fix: composite layer opacity/brightness widgets + CSS layout
Lint & Test / test (push) Successful in 1m7s
Lint & Test / test (push) Successful in 1m7s
- Fix opacity widget empty space (CSS selector .composite-layer-opacity → .composite-layer-opacity-container) - Replace brightness select dropdown with BindableScalarWidget (slider + VS toggle) - Legacy brightness_source_id auto-converted to BindableFloat on load - Add .composite-layer-brightness-container CSS rule
This commit is contained in:
@@ -268,7 +268,8 @@ def get_system_performance(_: AuthRequired):
|
||||
|
||||
# App-level metrics
|
||||
proc_mem = _process.memory_info()
|
||||
app_cpu = _process.cpu_percent(interval=None)
|
||||
# Process.cpu_percent() is per-core (0–N*100%); normalize to 0–100% scale
|
||||
app_cpu = _process.cpu_percent(interval=None) / (psutil.cpu_count(logical=True) or 1)
|
||||
app_ram_mb = round(proc_mem.rss / 1024 / 1024, 1)
|
||||
|
||||
gpu = None
|
||||
|
||||
@@ -38,7 +38,8 @@ def _collect_system_snapshot() -> dict:
|
||||
"ram_pct": mem.percent,
|
||||
"ram_used": round(mem.used / 1024 / 1024, 1),
|
||||
"ram_total": round(mem.total / 1024 / 1024, 1),
|
||||
"app_cpu": _process.cpu_percent(interval=None),
|
||||
# Process.cpu_percent() is per-core (0–N*100%); normalize to 0–100%
|
||||
"app_cpu": _process.cpu_percent(interval=None) / (psutil.cpu_count(logical=True) or 1),
|
||||
"app_ram": round(proc_mem.rss / 1024 / 1024, 1),
|
||||
"gpu_util": None,
|
||||
"gpu_temp": None,
|
||||
|
||||
@@ -368,12 +368,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Per-metric accent colors ── */
|
||||
.perf-chart-card {
|
||||
--perf-accent: var(--primary-color);
|
||||
--perf-accent-glow: transparent;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: 3px solid var(--perf-accent);
|
||||
border-radius: 6px;
|
||||
padding: 10px 0 0;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
transition: box-shadow var(--duration-normal) ease;
|
||||
}
|
||||
|
||||
.perf-chart-card:hover {
|
||||
box-shadow: 0 0 16px var(--perf-accent-glow);
|
||||
}
|
||||
|
||||
.perf-chart-card[data-metric="cpu"] {
|
||||
--perf-accent: #FF6B6B;
|
||||
--perf-accent-glow: rgba(255, 107, 107, 0.12);
|
||||
}
|
||||
|
||||
.perf-chart-card[data-metric="ram"] {
|
||||
--perf-accent: #A855F7;
|
||||
--perf-accent-glow: rgba(168, 85, 247, 0.12);
|
||||
}
|
||||
|
||||
.perf-chart-card[data-metric="gpu"] {
|
||||
--perf-accent: #10B981;
|
||||
--perf-accent-glow: rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
|
||||
.perf-chart-wrap {
|
||||
@@ -382,6 +407,16 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Subtle gradient wash in chart background */
|
||||
.perf-chart-wrap::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, var(--perf-accent-glow), transparent 60%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.perf-chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -395,7 +430,21 @@
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
color: var(--text-secondary);
|
||||
color: var(--perf-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Accent dot before label */
|
||||
.perf-chart-label::before {
|
||||
content: '';
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--perf-accent);
|
||||
box-shadow: 0 0 6px var(--perf-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.perf-chart-subtitle {
|
||||
@@ -414,10 +463,26 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Value display ── */
|
||||
.perf-chart-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-text-color);
|
||||
color: var(--perf-accent);
|
||||
font-family: var(--font-mono, monospace);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* App value shown as subdued tag in "both" mode */
|
||||
.perf-chart-value .perf-val-app {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
background: var(--hover-bg);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.perf-chart-label .color-picker-swatch {
|
||||
@@ -433,6 +498,7 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ── Mode toggle ── */
|
||||
.perf-mode-toggle {
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
|
||||
@@ -1980,7 +1980,8 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.composite-layer-opacity {
|
||||
.composite-layer-opacity-container,
|
||||
.composite-layer-brightness-container {
|
||||
flex: 1;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ let _compositeLayers: any[] = [];
|
||||
let _compositeAvailableSources: any[] = []; // non-composite sources for layer dropdowns
|
||||
let _compositeSourceEntitySelects: any[] = [];
|
||||
let _compositeBrightnessEntitySelects: any[] = [];
|
||||
let _compositeBrightnessWidgets: BindableScalarWidget[] = [];
|
||||
let _compositeBlendIconSelects: any[] = [];
|
||||
let _compositeCSPTEntitySelects: any[] = [];
|
||||
let _compositeOpacityWidgets: BindableScalarWidget[] = [];
|
||||
@@ -51,6 +52,8 @@ export function compositeDestroyEntitySelects() {
|
||||
_compositeCSPTEntitySelects = [];
|
||||
_compositeOpacityWidgets.forEach(w => w.destroy());
|
||||
_compositeOpacityWidgets = [];
|
||||
_compositeBrightnessWidgets.forEach(w => w.destroy());
|
||||
_compositeBrightnessWidgets = [];
|
||||
}
|
||||
|
||||
function _getCompositeBlendItems() {
|
||||
@@ -96,15 +99,10 @@ export function compositeRenderList() {
|
||||
const list = document.getElementById('composite-layers-list') as HTMLElement | null;
|
||||
if (!list) return;
|
||||
compositeDestroyEntitySelects();
|
||||
const vsList = _cachedValueSources || [];
|
||||
list.innerHTML = _compositeLayers.map((layer, i) => {
|
||||
const srcOptions = _compositeAvailableSources.map(s =>
|
||||
`<option value="${s.id}"${layer.source_id === s.id ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||
).join('');
|
||||
const vsOptions = `<option value="">${t('color_strip.composite.brightness.none')}</option>` +
|
||||
vsList.map(v =>
|
||||
`<option value="${v.id}"${layer.brightness_source_id === v.id ? ' selected' : ''}>${escapeHtml(v.name)}</option>`
|
||||
).join('');
|
||||
const csptList = _cachedCSPTemplates || [];
|
||||
const csptOptions = `<option value="">${t('common.none_no_cspt')}</option>` +
|
||||
csptList.map(tmpl =>
|
||||
@@ -151,7 +149,7 @@ export function compositeRenderList() {
|
||||
<label class="composite-layer-brightness-label">
|
||||
<span>${t('color_strip.composite.brightness')}:</span>
|
||||
</label>
|
||||
<select class="composite-layer-brightness" data-idx="${i}">${vsOptions}</select>
|
||||
<div class="composite-layer-brightness-container" data-idx="${i}"></div>
|
||||
</div>
|
||||
<div class="composite-layer-row">
|
||||
<label class="composite-layer-brightness-label">
|
||||
@@ -247,15 +245,22 @@ export function compositeRenderList() {
|
||||
}));
|
||||
});
|
||||
|
||||
// Attach EntitySelect to each layer's brightness dropdown
|
||||
list.querySelectorAll<HTMLSelectElement>('.composite-layer-brightness').forEach(sel => {
|
||||
_compositeBrightnessEntitySelects.push(new EntitySelect({
|
||||
target: sel,
|
||||
getItems: _getCompositeBrightnessItems,
|
||||
placeholder: t('palette.search'),
|
||||
allowNone: true,
|
||||
noneLabel: t('color_strip.composite.brightness.none'),
|
||||
}));
|
||||
// Create BindableScalarWidget for each layer's brightness
|
||||
list.querySelectorAll<HTMLElement>('.composite-layer-brightness-container').forEach((container, i) => {
|
||||
const layer = _compositeLayers[i];
|
||||
// Convert legacy brightness_source_id to BindableFloat
|
||||
const bfValue = layer?.brightness_source_id
|
||||
? { value: 1.0, source_id: layer.brightness_source_id }
|
||||
: 1.0;
|
||||
const widget = new BindableScalarWidget({
|
||||
container,
|
||||
min: 0, max: 1, step: 0.05, default: 1.0,
|
||||
idPrefix: `composite-brightness-${i}`,
|
||||
format: (v) => v.toFixed(2),
|
||||
valueSources: () => _cachedValueSources,
|
||||
});
|
||||
widget.setValue(bfValue);
|
||||
_compositeBrightnessWidgets.push(widget);
|
||||
});
|
||||
|
||||
// Attach EntitySelect to each layer's CSPT dropdown
|
||||
@@ -314,7 +319,6 @@ function _compositeLayersSyncFromDom() {
|
||||
const srcs = list.querySelectorAll<HTMLSelectElement>('.composite-layer-source');
|
||||
const blends = list.querySelectorAll<HTMLSelectElement>('.composite-layer-blend');
|
||||
const enableds = list.querySelectorAll<HTMLInputElement>('.composite-layer-enabled');
|
||||
const briSrcs = list.querySelectorAll<HTMLSelectElement>('.composite-layer-brightness');
|
||||
const csptSels = list.querySelectorAll<HTMLSelectElement>('.composite-layer-cspt');
|
||||
const starts = list.querySelectorAll<HTMLInputElement>('.composite-layer-start');
|
||||
const ends = list.querySelectorAll<HTMLInputElement>('.composite-layer-end');
|
||||
@@ -325,7 +329,13 @@ function _compositeLayersSyncFromDom() {
|
||||
_compositeLayers[i].blend_mode = blends[i].value;
|
||||
_compositeLayers[i].opacity = _compositeOpacityWidgets[i]?.getValue() ?? _compositeLayers[i].opacity;
|
||||
_compositeLayers[i].enabled = enableds[i].checked;
|
||||
_compositeLayers[i].brightness_source_id = briSrcs[i] ? (briSrcs[i].value || null) : null;
|
||||
// Brightness: read from widget (BindableFloat → extract source_id for legacy field)
|
||||
const briVal = _compositeBrightnessWidgets[i]?.getValue();
|
||||
if (briVal && typeof briVal === 'object' && 'source_id' in briVal) {
|
||||
_compositeLayers[i].brightness_source_id = briVal.source_id || null;
|
||||
} else {
|
||||
_compositeLayers[i].brightness_source_id = null;
|
||||
}
|
||||
_compositeLayers[i].processing_template_id = csptSels[i] ? (csptSels[i].value || null) : null;
|
||||
_compositeLayers[i].start = starts[i] ? (parseInt(starts[i].value) || 0) : 0;
|
||||
_compositeLayers[i].end = ends[i] ? (parseInt(ends[i].value) || 0) : 0;
|
||||
|
||||
@@ -17,6 +17,20 @@ const MAX_SAMPLES = 120;
|
||||
const CHART_KEYS = ['cpu', 'ram', 'gpu'];
|
||||
const PERF_MODE_KEY = 'perfMetricsMode';
|
||||
|
||||
/** Default accent colors per metric — distinct hues for visual identity. */
|
||||
const METRIC_COLORS: Record<string, string> = {
|
||||
cpu: '#FF6B6B', // warm coral
|
||||
ram: '#A855F7', // electric violet
|
||||
gpu: '#10B981', // emerald teal
|
||||
};
|
||||
|
||||
/** Complementary app/process line colors — clearly different hue per metric. */
|
||||
const APP_COLORS: Record<string, string> = {
|
||||
cpu: '#FFB347', // amber
|
||||
ram: '#60A5FA', // sky blue
|
||||
gpu: '#34D399', // mint
|
||||
};
|
||||
|
||||
type PerfMode = 'system' | 'app' | 'both';
|
||||
|
||||
let _pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -28,14 +42,12 @@ let _mode: PerfMode = (localStorage.getItem(PERF_MODE_KEY) as PerfMode) || 'both
|
||||
|
||||
function _getColor(key: string): string {
|
||||
return localStorage.getItem(`perfChartColor_${key}`)
|
||||
|| getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim()
|
||||
|| METRIC_COLORS[key]
|
||||
|| '#4CAF50';
|
||||
}
|
||||
|
||||
function _getAppColor(key: string): string {
|
||||
const base = _getColor(key);
|
||||
// Use a lighter/shifted version for the app line
|
||||
return base + '99'; // 60% opacity hex suffix
|
||||
return APP_COLORS[key] || _getColor(key) + '99';
|
||||
}
|
||||
|
||||
function _onChartColorChange(key: string, hex: string | null): void {
|
||||
@@ -53,10 +65,15 @@ function _onChartColorChange(key: string, hex: string | null): void {
|
||||
if (chart) {
|
||||
chart.data.datasets[0].borderColor = hex;
|
||||
chart.data.datasets[0].backgroundColor = hex + '26';
|
||||
chart.data.datasets[1].borderColor = hex + '99';
|
||||
chart.data.datasets[1].backgroundColor = hex + '14';
|
||||
// App line keeps its distinct hue (only update if user explicitly reset)
|
||||
const appColor = _getAppColor(key);
|
||||
chart.data.datasets[1].borderColor = appColor;
|
||||
chart.data.datasets[1].backgroundColor = appColor + '1A';
|
||||
chart.update();
|
||||
}
|
||||
// Update card accent color
|
||||
const card = document.querySelector(`.perf-chart-card[data-metric="${key}"]`) as HTMLElement | null;
|
||||
if (card) card.style.setProperty('--perf-accent', hex!);
|
||||
}
|
||||
|
||||
/** Build the 3-way toggle HTML for perf section header. */
|
||||
@@ -98,21 +115,21 @@ export function renderPerfSection(): string {
|
||||
}
|
||||
|
||||
return `<div class="perf-charts-grid">
|
||||
<div class="perf-chart-card">
|
||||
<div class="perf-chart-card" data-metric="cpu">
|
||||
<div class="perf-chart-header">
|
||||
<span class="perf-chart-label">${t('dashboard.perf.cpu')} ${createColorPicker({ id: 'perf-cpu', currentColor: _getColor('cpu'), onPick: undefined, 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-card" data-metric="ram">
|
||||
<div class="perf-chart-header">
|
||||
<span class="perf-chart-label">${t('dashboard.perf.ram')} ${createColorPicker({ id: 'perf-ram', currentColor: _getColor('ram'), onPick: undefined, 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-card" data-metric="gpu" 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'), onPick: undefined, anchor: 'left', showReset: true })}</span>
|
||||
<span class="perf-chart-value" id="perf-gpu-value">-</span>
|
||||
@@ -126,6 +143,7 @@ function _createChart(canvasId: string, key: string): any {
|
||||
const ctx = document.getElementById(canvasId) as HTMLCanvasElement | null;
|
||||
if (!ctx) return null;
|
||||
const color = _getColor(key);
|
||||
const appColor = _getAppColor(key);
|
||||
const showSystem = _mode === 'system' || _mode === 'both';
|
||||
const showApp = _mode === 'app' || _mode === 'both';
|
||||
return new Chart(ctx, {
|
||||
@@ -134,21 +152,21 @@ function _createChart(canvasId: string, key: string): any {
|
||||
labels: Array(MAX_SAMPLES).fill(''),
|
||||
datasets: [
|
||||
{
|
||||
// System-wide dataset
|
||||
// System-wide dataset — bold solid line
|
||||
data: [],
|
||||
borderColor: color,
|
||||
backgroundColor: color + '26',
|
||||
borderWidth: 1.5,
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
hidden: !showSystem,
|
||||
},
|
||||
{
|
||||
// App-level dataset (dashed line)
|
||||
// App-level dataset — distinct hue, dashed
|
||||
data: [],
|
||||
borderColor: color + '99',
|
||||
backgroundColor: color + '14',
|
||||
borderColor: appColor,
|
||||
backgroundColor: appColor + '1A',
|
||||
borderWidth: 1.5,
|
||||
borderDash: [4, 3],
|
||||
tension: 0.3,
|
||||
@@ -257,15 +275,25 @@ function _pushSample(key: string, sysValue: number, appValue: number | null): vo
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
/** Format the value display based on mode. */
|
||||
/** Format the value display based on mode.
|
||||
* In "both" mode, returns HTML with a styled app-value tag. */
|
||||
function _formatValue(sysVal: string, appVal: string | null): string {
|
||||
if (_mode === 'system') return sysVal;
|
||||
if (_mode === 'app') return appVal ?? '-';
|
||||
// 'both': show both
|
||||
if (appVal != null) return `${sysVal} / ${appVal}`;
|
||||
// 'both': system value prominent, app value as subdued badge
|
||||
if (appVal != null) return `${sysVal} <span class="perf-val-app">${appVal}</span>`;
|
||||
return sysVal;
|
||||
}
|
||||
|
||||
/** Apply formatted value — uses innerHTML when in "both" mode for styled tags. */
|
||||
function _setValueEl(el: HTMLElement, html: string): void {
|
||||
if (_mode === 'both') {
|
||||
el.innerHTML = html;
|
||||
} else {
|
||||
el.textContent = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function _fetchPerformance(): Promise<void> {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/system/performance`, { headers: getHeaders() });
|
||||
@@ -276,10 +304,10 @@ async function _fetchPerformance(): Promise<void> {
|
||||
_pushSample('cpu', data.cpu_percent, data.app_cpu_percent);
|
||||
const cpuEl = document.getElementById('perf-cpu-value');
|
||||
if (cpuEl) {
|
||||
cpuEl.textContent = _formatValue(
|
||||
_setValueEl(cpuEl, _formatValue(
|
||||
`${data.cpu_percent.toFixed(0)}%`,
|
||||
`${data.app_cpu_percent.toFixed(0)}%`
|
||||
);
|
||||
));
|
||||
}
|
||||
if (data.cpu_name) {
|
||||
const nameEl = document.getElementById('perf-cpu-name');
|
||||
@@ -296,10 +324,10 @@ async function _fetchPerformance(): Promise<void> {
|
||||
const usedGb = (data.ram_used_mb / 1024).toFixed(1);
|
||||
const totalGb = (data.ram_total_mb / 1024).toFixed(1);
|
||||
const appMb = data.app_ram_mb.toFixed(0);
|
||||
ramEl.textContent = _formatValue(
|
||||
_setValueEl(ramEl, _formatValue(
|
||||
`${usedGb}/${totalGb} GB`,
|
||||
`${appMb} MB`
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
// GPU
|
||||
@@ -317,7 +345,7 @@ async function _fetchPerformance(): Promise<void> {
|
||||
const appText = data.gpu.app_memory_mb != null
|
||||
? `${data.gpu.app_memory_mb.toFixed(0)} MB VRAM`
|
||||
: null;
|
||||
gpuEl.textContent = _formatValue(sysText, appText);
|
||||
_setValueEl(gpuEl, _formatValue(sysText, appText));
|
||||
}
|
||||
if (data.gpu.name) {
|
||||
const nameEl = document.getElementById('perf-gpu-name');
|
||||
|
||||
Reference in New Issue
Block a user