This commit is contained in:
Mateusz Gruszczyński
2026-09-16 22:22:09 +02:00
parent c7d47db64e
commit bb39ab9dfa
27 changed files with 523 additions and 319 deletions
+14 -3
View File
@@ -12,12 +12,23 @@
</head>
<body class="public-custom-chart-page">
<main class="public-custom-chart-shell">
<section class="panel chart-panel history-chart-card public-custom-chart-card" aria-live="polite">
<div class="chart-title-row">
<div>
<section class="panel chart-panel chart-card public-custom-chart-card" aria-live="polite">
<div class="chart-title-row public-chart-title-row">
<div class="public-chart-heading">
<h3 id="publicChartTitle">Custom chart</h3>
<p id="publicChartHint">Loading…</p>
</div>
<label class="public-chart-range-control">
<span id="publicChartRangeLabel">Range</span>
<select id="publicChartHours" aria-label="Chart range">
<option value="6">6 hours</option>
<option value="24">24 hours</option>
<option value="168">7 days</option>
<option value="720">30 days</option>
<option value="2160">90 days</option>
<option value="8760">1 year</option>
</select>
</label>
</div>
<div class="chart-wrap public-custom-chart-wrap">
<canvas id="publicCustomChart" width="1200" height="520" tabindex="0" aria-label="Custom chart"></canvas>
+86 -5
View File
@@ -3,6 +3,7 @@
const PUBLIC_CHART_COLORS = ['--accent', '--info', '--warning', '--purple', '--danger', '--teal', '--orange', '--blue'];
const $ = selector => document.querySelector(selector);
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
const PUBLIC_CHART_RANGES = [6, 24, 168, 720, 2160, 8760];
function basePathFromScript() {
const src = document.currentScript?.src || '';
@@ -41,7 +42,7 @@ function preciseTime(timestamp, locale) {
function prepareCanvas(canvas, height = 520) {
const wrap = canvas.parentElement;
const width = Math.max(720, Math.floor(wrap?.clientWidth || canvas.getBoundingClientRect().width || 720));
const actualHeight = Math.max(320, Math.floor(wrap?.clientHeight || height));
const actualHeight = Math.max(180, Math.floor(wrap?.clientHeight || height));
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(actualHeight * dpr);
@@ -194,12 +195,64 @@ function drawChart(canvas, payload, locale) {
bindTooltip(canvas, series, { pad, width, height, firstTs, lastTs }, locale);
}
async function loadPublicChart() {
const token = decodeURIComponent(location.pathname.split('/').filter(Boolean).pop() || '');
let publicChartPayload = null;
let publicChartLocale = 'en-GB';
let publicChartResizeFrame = 0;
function publicChartToken() {
return decodeURIComponent(location.pathname.split('/').filter(Boolean).pop() || '');
}
function requestedPublicChartHours() {
const value = Number(new URLSearchParams(location.search).get('hours'));
return Number.isInteger(value) && value >= 1 && value <= 87600 ? value : null;
}
function setPublicRangeLanguage(lang) {
const pl = lang === 'pl';
const label = $('#publicChartRangeLabel');
const select = $('#publicChartHours');
if (label) label.textContent = pl ? 'Zakres' : 'Range';
if (!select) return;
const labels = pl
? ['6 godzin', '24 godziny', '7 dni', '30 dni', '90 dni', '1 rok']
: ['6 hours', '24 hours', '7 days', '30 days', '90 days', '1 year'];
[...select.options].forEach((option, index) => { if (labels[index]) option.textContent = labels[index]; });
select.setAttribute('aria-label', pl ? 'Zakres wykresu' : 'Chart range');
}
function syncPublicRangeControl(hours) {
const select = $('#publicChartHours');
if (!select) return;
const value = String(hours);
if (![...select.options].some(option => option.value === value)) {
const option = document.createElement('option');
option.value = value;
option.textContent = `${value} h`;
select.append(option);
}
select.value = value;
}
function publicChartApiPath(token, hours) {
const value = Number(hours);
const hasHours = hours !== null && hours !== undefined && Number.isInteger(value) && value >= 1 && value <= 87600;
const query = hasHours ? `?hours=${encodeURIComponent(String(value))}` : '';
return `/api/public/charts/custom/${encodeURIComponent(token)}${query}`;
}
function redrawPublicChart() {
if (!publicChartPayload) return;
drawChart($('#publicCustomChart'), publicChartPayload, publicChartLocale);
}
async function loadPublicChart(hours = requestedPublicChartHours()) {
const token = publicChartToken();
const title = $('#publicChartTitle');
const hint = $('#publicChartHint');
const error = $('#publicChartError');
const canvas = $('#publicCustomChart');
const range = $('#publicChartHours');
if (!token.startsWith('chart_')) {
title.textContent = 'Custom chart';
@@ -210,7 +263,9 @@ async function loadPublicChart() {
return;
}
const response = await fetch(withBase(`/api/public/charts/custom/${encodeURIComponent(token)}`), { headers: { Accept: 'application/json' }, cache: 'no-store' });
if (range) range.disabled = true;
error.hidden = true;
const response = await fetch(withBase(publicChartApiPath(token, hours)), { headers: { Accept: 'application/json' }, cache: 'no-store' });
if (!response.ok) {
let message = `${response.status}`;
try { message = (await response.json()).error || message; } catch (_) { }
@@ -223,14 +278,40 @@ async function loadPublicChart() {
title.textContent = payload.title || (lang === 'pl' ? 'Wykres niestandardowy' : 'Custom chart');
hint.textContent = payload.hint || '';
canvas.setAttribute('aria-label', title.textContent);
setPublicRangeLanguage(lang);
syncPublicRangeControl(payload.hours);
publicChartPayload = payload;
publicChartLocale = locale;
drawChart(canvas, payload, locale);
window.addEventListener('resize', () => drawChart(canvas, payload, locale));
if (range) range.disabled = false;
}
$('#publicChartHours')?.addEventListener('change', async event => {
const hours = Number(event.currentTarget.value);
if (!Number.isInteger(hours) || hours < 1 || hours > 87600) return;
const url = new URL(location.href);
url.searchParams.set('hours', String(hours));
history.replaceState(null, '', url);
try {
await loadPublicChart(hours);
} catch (error) {
const host = $('#publicChartError');
host.hidden = false;
host.textContent = `${document.documentElement.lang === 'pl' ? 'Nie udało się wczytać wykresu' : 'Unable to load chart'}: ${error.message}`;
event.currentTarget.disabled = false;
}
});
window.addEventListener('resize', () => {
cancelAnimationFrame(publicChartResizeFrame);
publicChartResizeFrame = requestAnimationFrame(redrawPublicChart);
});
loadPublicChart().catch(error => {
const host = $('#publicChartError');
host.hidden = false;
host.textContent = `Unable to load chart: ${error.message}`;
$('#publicChartHint').textContent = '';
$('#publicChartHours').disabled = false;
drawEmpty($('#publicCustomChart'), host.textContent);
});
+78 -78
View File
@@ -189,7 +189,7 @@
data-i18n="flow.import">Import Flow</button><button class="primary" type="button" data-action="new-flow"
data-i18n="flow.new">New Flow</button></div>
</div>
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
<nav class="segmented-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
<button type="button" class="active" data-go="flows" data-i18n="nav.flows">Flow</button>
<button type="button" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
<button type="button" data-go="automations" data-i18n="nav.automations">Automations</button>
@@ -215,7 +215,7 @@
<h1 data-i18n="nav.schedules">Schedules</h1>
</div><button class="secondary" data-open="scheduleDialog" data-i18n="actions.add">Add</button>
</div>
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
<nav class="segmented-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
<button type="button" data-go="flows" data-i18n="nav.flows">Flow</button>
<button type="button" class="active" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
<button type="button" data-go="automations" data-i18n="nav.automations">Automations</button>
@@ -248,7 +248,7 @@
<h1 data-i18n="nav.automations">Automations</h1>
</div><button class="secondary" data-open="automationDialog" data-i18n="actions.add">Add</button>
</div>
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
<nav class="segmented-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
<button type="button" data-go="flows" data-i18n="nav.flows">Flow</button>
<button type="button" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
<button type="button" class="active" data-go="automations" data-i18n="nav.automations">Automations</button>
@@ -322,15 +322,15 @@
<h1 data-i18n="history.title">Climate history</h1>
</div>
</div>
<div class="history-tabs" id="historyTabs" role="tablist">
<button data-history-tab="overview" data-i18n="history.tabOverview">Overview</button>
<button data-history-tab="zones" data-i18n="history.tabZones">Zones</button>
<button data-history-tab="devices" data-i18n="history.tabDevices">GREE devices</button>
<button data-history-tab="energy" data-i18n="energy.title">Energy</button>
<button data-history-tab="network" data-i18n="history.tabNetwork">Pings</button>
<button data-history-tab="sensors" data-i18n="history.tabSensors">HA sensors</button>
<button data-history-tab="custom" data-i18n="history.tabCustom">Custom chart</button>
</div>
<nav class="segmented-tabs" id="historyTabs" role="tablist" aria-label="History navigation">
<button type="button" data-history-tab="overview" data-i18n="history.tabOverview">Overview</button>
<button type="button" data-history-tab="zones" data-i18n="history.tabZones">Zones</button>
<button type="button" data-history-tab="devices" data-i18n="history.tabDevices">GREE devices</button>
<button type="button" data-history-tab="energy" data-i18n="energy.title">Energy</button>
<button type="button" data-history-tab="network" data-i18n="history.tabNetwork">Pings</button>
<button type="button" data-history-tab="sensors" data-i18n="history.tabSensors">HA sensors</button>
<button type="button" data-history-tab="custom" data-i18n="history.tabCustom">Custom chart</button>
</nav>
<div class="panel chart-panel history-toolbar-panel">
<div class="chart-toolbar">
<div id="historyContextControls" class="history-context-controls"></div>
@@ -361,16 +361,16 @@
</div>
<p class="lead" data-i18n="night.description">Configure quieter thermostat behavior for sleeping hours without
mixing it with application settings.</p>
<form class="settings-form standalone-settings-form" id="nightModeForm">
<section class="panel settings-block">
<div class="settings-block-head">
<form class="config-form full-width-form" id="nightModeForm">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.nightMode">Night mode</h3>
<p data-i18n="settings.nightModeHint">During selected hours the thermostat limits fan speed and can
request Quiet on supported units.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="night_mode_enabled"> <span
data-i18n="settings.nightModeEnabled">Enable night mode</span></label>
<label><span data-i18n="settings.nightStart">Start</span><input type="time" name="night_mode_start"
@@ -391,7 +391,7 @@
data-i18n="settings.nightNativeSleep">Use native Sleep when supported</span></label>
</div>
</section>
<div class="settings-save-bar"><span data-i18n="night.saveHint">Save night-mode changes.</span><button
<div class="sticky-form-actions"><span data-i18n="night.saveHint">Save night-mode changes.</span><button
class="primary" type="submit" data-i18n="actions.save">Save</button></div>
</form>
</section>
@@ -404,16 +404,16 @@
</div>
<p class="lead" data-i18n="homeAssistant.description">Connection, temperature sensors, friendly aliases and access
tokens for the Home Assistant integration.</p>
<form class="settings-form standalone-settings-form" id="homeAssistantForm">
<section class="panel settings-block">
<div class="settings-block-head">
<form class="config-form full-width-form" id="homeAssistantForm">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.haConnection">Połączenie z Home Assistant</h3>
<p data-i18n="settings.haConnectionHint">Adres serwera i dane dostępu używane przez wszystkie funkcje Home
Assistant.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<div id="haSupervisorStatus" class="ha-supervisor-status wide" role="status" aria-live="polite" hidden></div>
<label class="wide" data-ha-manual-field><span>URL</span><input type="url" name="ha_url"
placeholder="http://homeassistant.local:8123"></label>
@@ -430,15 +430,15 @@
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.haThermostatSources">Źródła temperatury sterowania</h3>
<p data-i18n="settings.haThermostatSourcesHint">Te ustawienia należą do logiki termostatów i źródeł
temperatury, nie do wspólnych wejść Flow.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="wide"><span data-i18n="settings.outdoorEntity">Global outdoor temperature entity_id</span><input
name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label>
<p class="field-note wide" data-i18n="settings.outdoorEntityHint">Global Home Assistant outdoor temperature sensor. Zones use it by default and may override it with another outdoor sensor in zone settings.</p>
@@ -452,8 +452,8 @@
</div>
</section>
<section class="panel settings-block flow-shared-settings" id="flowSharedInputsSettings">
<div class="settings-block-head flow-shared-settings-head">
<section class="panel form-section flow-shared-settings" id="flowSharedInputsSettings">
<div class="form-section-head flow-shared-settings-head">
<div>
<h3 data-i18n="flow.sharedInputsTitle">Wspólne wejścia Flow</h3>
<p data-i18n="flow.sharedInputsHint">Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow.
@@ -465,8 +465,8 @@
<div id="flowSharedInputList" class="flow-shared-input-list"></div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.sensorAliases">Sensor aliases</h3>
<p data-i18n="settings.sensorAliasesHint">Badges show whether an entity is collected as metrics, used by
@@ -481,8 +481,8 @@
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.haIntegrationAccess">Home Assistant integration access</h3>
<p data-i18n="settings.haIntegrationHint">Create a controller token and paste it into the GREE Controller
@@ -495,7 +495,7 @@
data-i18n="settings.newToken">Create new token</button></div>
</div>
</section>
<div class="settings-save-bar"><span data-i18n="homeAssistant.saveHint">Save Home Assistant and sensor
<div class="sticky-form-actions"><span data-i18n="homeAssistant.saveHint">Save Home Assistant and sensor
changes.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
</form>
</section>
@@ -514,31 +514,31 @@
data-i18n="settings.greeSettings">GREE</button>
<button type="button" role="tab" aria-selected="false" data-settings-tab="cloud">GREE Cloud</button>
</div>
<form class="settings-form" id="settingsForm" novalidate>
<form class="config-form" id="settingsForm" novalidate>
<div class="settings-pane" data-settings-pane="app">
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.applicationRuntime">Application runtime</h3>
<p data-i18n="settings.applicationRuntimeHint">Application-wide behavior that does not change GREE
transport or discovery.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="simulator_enabled"> <span
data-i18n="settings.simulationMode">Simulation mode</span></label>
<p class="field-note wide warning-note" data-i18n="settings.simulationModeHint">When enabled, a prominent
warning is shown throughout the application.</p>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="notifications.title">Notifications</h3>
<p data-i18n="notifications.hint">Send important events or only problems/anomalies.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="notifications_enabled"> <span
data-i18n="notifications.enable">Enable notifications</span></label>
<label><span data-i18n="notifications.mode">Mode</span><select name="notifications_mode">
@@ -600,15 +600,15 @@
data-i18n="notifications.test">Send test notification</button></div>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.metricsAndLogs">Metrics and logs</h3>
<p data-i18n="settings.compactionHint">SQLite keeps recent data locally and compacts older samples to
the resolution used by charts.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label><span data-i18n="settings.retentionDays">Local metric retention (days)</span><input type="number"
name="history_retention_days" min="1" max="3650" required></label>
<label><span data-i18n="settings.eventRetentionDays">Event/log retention (days)</span><input type="number"
@@ -617,15 +617,15 @@
data-i18n="settings.compaction">Compact old metrics</span></label>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.influx">Long-term InfluxDB history</h3>
<p data-i18n="settings.influxHint">Optional archive for older history. InfluxDB 1.x and 2.x are
supported.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="influx_enabled"> <span
data-i18n="settings.influxEnabled">Enable InfluxDB archive</span></label>
<label><span data-i18n="settings.influxVersion">InfluxDB version</span><select name="influx_version">
@@ -654,14 +654,14 @@
</div>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.debug">On-screen debug</h3>
<p data-i18n="settings.debugHint">Live diagnostics displayed on every application page.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check"><input type="checkbox" name="debug_overlay_enabled"> <span
data-i18n="settings.debugOverlay">Show debug window on every page</span></label>
<label class="check"><input type="checkbox" name="debug_gree_frames"> <span
@@ -672,8 +672,8 @@
data-i18n="settings.debugCloudMqtt">Trace GREE Cloud MQTT</span></label>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.backup">Configuration backup</h3>
<p class="warning-note" data-i18n="settings.backupHint">Export/import application configuration.
@@ -689,15 +689,15 @@
<div class="panel system-panel" id="systemInfo"></div>
</div>
<div class="settings-pane" data-settings-pane="gree" hidden>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.greeConnection">GREE connection</h3>
<p data-i18n="settings.greeConnectionHint">Controller identity, polling intervals and LAN discovery
transport.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label><span data-i18n="settings.clientId">Client identifier</span><input name="controller_id"
required></label>
<label><span data-i18n="settings.pollInterval">Poll interval (s)</span><input type="number"
@@ -710,29 +710,29 @@
name="discovery_timeout_ms" min="300" max="30000" required></label>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.connectivityTitle">Connectivity metrics</h3>
<p data-i18n="settings.connectivityHint">Periodic GREE UDP round-trip measurements for Local/LAN units. Results are stored in History.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="ping_metrics_enabled"> <span data-i18n="settings.connectivityEnable">Enable local unit ping measurements</span></label>
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="ping_interval_seconds" min="10" max="3600" value="60"></label>
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="ping_sample_count" min="1" max="10" value="3"></label>
<p class="field-note wide" data-i18n="settings.connectivityLocalOnly">Only Local/LAN installations are probed. Cloud devices are not pinged directly.</p>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.greeCommands">GREE commands</h3>
<p data-i18n="settings.suppressBeepHint">Only changed properties are sent. Buzzer suppression is also
requested when supported by the unit firmware.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="suppress_device_beep"> <span
data-i18n="settings.suppressBeep">Try to suppress command beeps</span></label>
<label class="check wide"><input type="checkbox" name="compressor_protection_enabled"> <span
@@ -744,8 +744,8 @@
Thermostats.</p>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.greeTraffic">GREE traffic</h3>
<p data-i18n="settings.greeTrafficHint">Live counters of UDP frames received from configured air
@@ -756,9 +756,9 @@
</section>
</div>
<div class="settings-pane" data-settings-pane="cloud" hidden>
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.cloudAccountTitle">GREE Cloud account</h3><p data-i18n="settings.cloudAccountHint">REST login and device discovery use the selected regional GREE service. The password is never returned by the API after it is saved.</p></div></div>
<div class="settings-grid">
<section class="panel form-section">
<div class="form-section-head"><div><h3 data-i18n="settings.cloudAccountTitle">GREE Cloud account</h3><p data-i18n="settings.cloudAccountHint">REST login and device discovery use the selected regional GREE service. The password is never returned by the API after it is saved.</p></div></div>
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="gree_cloud_enabled"> <span data-i18n="settings.cloudEnable">Enable GREE Cloud</span></label>
<label><span data-i18n="settings.cloudRegion">Region</span><select name="gree_cloud_region">
<option>Australia</option><option>China Mainland</option><option>East South Asia</option><option selected>Europe</option><option>India</option><option>Latin American</option><option>Middle East</option><option>North American</option><option>Russia</option><option>South American</option>
@@ -787,14 +787,14 @@
<div id="greeCloudTestResult" class="inline-alert wide" hidden></div>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head">
<section class="panel form-section">
<div class="form-section-head">
<div>
<h3 data-i18n="settings.cloudConnectivityTitle">Cloud connectivity metrics</h3>
<p data-i18n="settings.cloudConnectivityHint">Optional. Disabled by default. Measures REST login response and MQTT PINGRESP round-trip.</p>
</div>
</div>
<div class="settings-grid">
<div class="form-grid">
<label class="check wide"><input type="checkbox" name="gree_cloud_connectivity_metrics_enabled"> <span data-i18n="settings.cloudConnectivityEnable">Measure Cloud REST/MQTT connectivity</span></label>
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="gree_cloud_connectivity_metrics_interval_seconds" min="30" max="3600" value="300"></label>
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="gree_cloud_connectivity_metrics_sample_count" min="1" max="10" value="3"></label>
@@ -802,7 +802,7 @@
</div>
</section>
</div>
<div class="settings-save-bar"><span data-i18n="settings.saveHint">Save changes made in the blocks
<div class="sticky-form-actions"><span data-i18n="settings.saveHint">Save changes made in the blocks
above.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
</form>
</section>
@@ -878,16 +878,16 @@
<aside class="flow-palette">
<div class="flow-palette-head"><strong data-i18n="flow.blocks">Blocks</strong><small
data-i18n="flow.paletteHint">Add a block, then connect its output to the next block.</small></div>
<div class="flow-palette-group flow-palette-trigger"><span data-i18n="flow.triggers">Wyzwalacze</span>
<div class="flow-palette-group block-category-trigger"><span data-i18n="flow.triggers">Wyzwalacze</span>
<button type="button" data-flow-add="cron_trigger" data-i18n="flow.node.cronTrigger">CRON</button>
</div>
<div class="flow-palette-group flow-palette-time"><span data-i18n="flow.time">Czas</span>
<div class="flow-palette-group block-category-time"><span data-i18n="flow.time">Czas</span>
<button type="button" data-flow-add="weekday" data-i18n="flow.node.weekday">Dni tygodnia</button><button
type="button" data-flow-add="time_range" data-i18n="flow.node.timeRange">Przedział godzin</button><button
type="button" data-flow-add="date_range" data-i18n="flow.node.dateRange">Zakres dat</button><button
type="button" data-flow-add="night_mode" data-i18n="flow.node.nightMode">Tryb nocny</button>
</div>
<div class="flow-palette-group flow-palette-timeop"><span data-i18n="flow.timeOps">Operacje czasu</span>
<div class="flow-palette-group block-category-timeop"><span data-i18n="flow.timeOps">Operacje czasu</span>
<button type="button" data-flow-add="stable_for" data-i18n="flow.node.stableFor">Utrzymuje się
przez…</button><button type="button" data-flow-add="state_duration" data-i18n="flow.node.stateDuration">Czas
trwania stanu</button><button type="button" data-flow-add="on_change" data-i18n="flow.node.onChange">Tylko
@@ -895,7 +895,7 @@
X razy / okres</button><button type="button" data-flow-add="delay"
data-i18n="flow.node.delay">Odczekaj…</button>
</div>
<div class="flow-palette-group flow-palette-sensor"><span data-i18n="flow.sensors">Sensory / wartości</span>
<div class="flow-palette-group block-category-sensor"><span data-i18n="flow.sensors">Sensory / wartości</span>
<button type="button" data-flow-add="outdoor_temperature" data-i18n="flow.node.outdoorTemperature">Temperatura
zewn.</button><button type="button" data-flow-add="device_temperature"
data-i18n="flow.node.deviceTemperature">Temperatura urządzenia</button><button type="button"
@@ -913,17 +913,17 @@
data-flow-add="rolling_stat" data-i18n="flow.node.rollingStat">Średnia / mediana</button><button
type="button" data-flow-add="oscillates" data-i18n="flow.node.oscillates">Wartość oscyluje</button>
</div>
<div class="flow-palette-group flow-palette-logic"><span data-i18n="flow.logic">Logika</span><button
<div class="flow-palette-group block-category-logic"><span data-i18n="flow.logic">Logika</span><button
type="button" data-flow-add="logic_and" data-i18n="flow.node.and">AND</button><button type="button"
data-flow-add="logic_or" data-i18n="flow.node.or">OR</button><button type="button" data-flow-add="logic_not"
data-i18n="flow.node.not">NOT</button></div>
<div class="flow-palette-group flow-palette-action"><span data-i18n="flow.actions">Akcje</span>
<div class="flow-palette-group block-category-action"><span data-i18n="flow.actions">Akcje</span>
<button type="button" data-flow-add="zone_thermostat"
data-i18n="flow.node.thermostat">Termostat</button><button type="button" data-flow-add="device_action"
data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button" data-flow-add="group_action"
data-i18n="flow.node.group">Grupa</button>
</div>
<div class="flow-palette-group flow-palette-haaction"><span data-i18n="flow.haActions">Home Assistant</span>
<div class="flow-palette-group block-category-haaction"><span data-i18n="flow.haActions">Home Assistant</span>
<button type="button" data-flow-add="ha_service_action" data-i18n="flow.node.haServiceAction">Home Assistant:
usługa</button>
</div>
@@ -1233,9 +1233,9 @@
</div>
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
<div class="technical-device-grid" id="deviceDetailsMeta"></div>
<section class="settings-block">
<div class="settings-block-head"><div><h3 data-i18n="energy.title">Energy</h3><p data-i18n="energy.sourceHint">Choose the cumulative energy source used for charts and period totals.</p></div></div>
<div class="settings-grid">
<section class="form-section">
<div class="form-section-head"><div><h3 data-i18n="energy.title">Energy</h3><p data-i18n="energy.sourceHint">Choose the cumulative energy source used for charts and period totals.</p></div></div>
<div class="form-grid">
<label><span data-i18n="energy.source">Energy source</span><select name="energy_source">
<option value="auto" data-i18n="energy.auto">Auto</option>
<option value="gree_cloud">GREE Cloud</option>
@@ -1257,7 +1257,7 @@
<div id="deviceGroupsList" class="installation-list"></div>
<form id="deviceGroupForm" class="installation-form">
<input type="hidden" name="id">
<div class="settings-grid">
<div class="form-grid">
<label><span data-i18n="devices.installationName">Installation name</span><input name="name" maxlength="80" required></label>
<label><span data-i18n="devices.installationType">Installation type</span><select name="kind"><option value="split" data-i18n="devices.split">Split</option><option value="multisplit" data-i18n="devices.multisplit">Multisplit</option></select></label>
<div class="wide"><span class="field-label" data-i18n="devices.installationDevices">Indoor units</span><div id="deviceGroupDeviceChoices" class="installation-device-choices"></div></div>
+6 -6
View File
@@ -191,7 +191,7 @@ function setChartSeriesHidden(chartId, item, index, hidden) {
function updateChartZoomControls(id) {
const canvas = document.getElementById(id);
const card = canvas?.closest('.history-chart-card');
const card = canvas?.closest('.chart-card');
if (!card) return;
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
const reset = card.querySelector('[data-chart-zoom="reset"]');
@@ -319,11 +319,11 @@ function closeChartPreview(card) {
function toggleChartFullscreen(id) {
if (!window.matchMedia('(min-width: 761px)').matches) return;
const canvas = document.getElementById(id);
const card = canvas?.closest('.history-chart-card');
const card = canvas?.closest('.chart-card');
if (!card) return;
const active = chartCardIsFullscreen(card);
const opened = document.querySelector('.history-chart-card.chart-fullscreen-fallback');
const opened = document.querySelector('.chart-card.chart-fullscreen-fallback');
if (opened && opened !== card) closeChartPreview(opened);
if (active) {
@@ -356,7 +356,7 @@ function prepareCanvas(canvas, height) {
const rect = canvas.getBoundingClientRect();
const wrap = canvas.parentElement;
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card'));
const fullscreen = chartCardIsFullscreen(canvas.closest('.chart-card'));
const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0;
const actualHeight = wrapHeight || height;
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM);
@@ -724,7 +724,7 @@ function renderLegend(host, series) {
function historyChartMarkup(id, title, hint, compact = false) {
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
const percent = Math.round(zoom * 100);
return `<div class="panel chart-panel history-chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
return `<div class="panel chart-panel chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
}
function historySeriesColor(index) {
@@ -733,6 +733,6 @@ function historySeriesColor(index) {
document.addEventListener('keydown', event => {
if (event.key === 'Escape') closeChartPreview(document.querySelector('.history-chart-card.chart-fullscreen-fallback'));
if (event.key === 'Escape') closeChartPreview(document.querySelector('.chart-card.chart-fullscreen-fallback'));
});
+1 -1
View File
@@ -31,7 +31,7 @@ const app = {
languages: [], translations: {}, locales: {},
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyNetworkTarget: 'all', historyNetworkShowJitter: true, historyNetwork: [], historyNetworkTargets: [], historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
customChartSeries: [], savedCharts: [], customChartEditingId: null, customChartNameDraft: null, chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
haManualFallbackVisible: false, haSupervisorTestState: null, haEntityCatalog: [], haEntityCatalogConfigured: false, haEntityCatalogLoadedAt: 0, haEntityCatalogLoading: false,
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
+1 -1
View File
@@ -377,7 +377,7 @@ function renderSimulationPage() {
return `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax + bend} ${ay}, ${bx - bend} ${by}, ${bx} ${by}"/>`;
};
const verticalLink = (ax, ay, bx, by, cls = 'bus') => `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax} ${ay + 35}, ${bx} ${by - 35}, ${bx} ${by}"/>`;
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="flow-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="flow-port in"></i><i class="flow-port out"></i></article>`;
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="diagram-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="diagram-port in"></i><i class="diagram-port out"></i></article>`;
const globalHouseLeft = 305, globalNightLeft = 565, globalTop = 28;
nodes.push(node({ left: globalHouseLeft, top: globalTop, kind: 'logic global', eyebrow: tr('simulation.globalInput'), title: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), meta: tr('simulation.strategy', { strategy: (plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint') }), badge: tr('simulation.house') }));
+1 -1
View File
@@ -537,7 +537,7 @@ function zoneCard(zone, detailed = true) {
const groupText = groupNames.length ? groupNames.join(' · ') : tr('zones.noGroup');
const policy = zone.inherit_house_mode ? tr('zones.followHouse') : tr(zone.mode === 'heat' ? 'zones.heatOnly' : 'zones.coolOnly');
return `<article class="list-card zone-config-card ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
<div class="zone-config-status">
<div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span>${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div></div>
<div class="zone-config-runtime"><span class="badge ${workingDemand ? 'active' : ''}">${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div>
+1 -1
View File
@@ -18,7 +18,7 @@ document.addEventListener('click', async event => {
if (hours && $('#historyHours')) $('#historyHours').value = hours;
else if ($('#historyHours')) $('#historyHours').value = '24';
const dialog = historyRoute.closest('dialog');
closeChartPreview(dialog?.querySelector('.history-chart-card.chart-fullscreen-fallback'));
closeChartPreview(dialog?.querySelector('.chart-card.chart-fullscreen-fallback'));
if (dialog?.open) dialog.close();
app.historyTab = 'overview';
showView('history');
+5 -5
View File
@@ -98,7 +98,7 @@ function renderFlows() {
const host = $('#flowList'); if (!host) return;
const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length });
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled && !flow.draft ? '' : 'is-disabled'} ${flow.draft ? 'is-draft' : ''}" data-flow-card-id="${esc(flow.id)}" tabindex="0" aria-label="${esc(`${tr('flow.openEditor')}: ${flow.name}`)}">
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="zone-enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="zone-enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
<div class="card-stats"><div class="card-stat"><small>${esc(tr('flow.blocks'))}</small><strong>${flow.nodes?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.schedules'))}</small><strong>${flow.compiled_schedule_ids?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.automations'))}</small><strong>${flow.compiled_automation_ids?.length || 0}</strong></div></div>
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
@@ -426,7 +426,7 @@ function renderFlowBlockLibrary(filter = '') {
const title = group.querySelector(':scope > span')?.textContent?.trim() || '';
const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query));
if (!buttons.length) return '';
const categoryClass = [...group.classList].find(name => name.startsWith('flow-palette-') && name !== 'flow-palette-group') || '';
const categoryClass = [...group.classList].find(name => name.startsWith('block-category-')) || '';
return `<section class="flow-block-library-group ${esc(categoryClass)}"><h3>${esc(title)}</h3><div>${buttons.map(button => `<button type="button" class="secondary" data-flow-add="${esc(button.dataset.flowAdd)}">${esc(button.textContent.trim())}</button>`).join('')}</div></section>`;
}).join('') || `<div class="empty compact"><strong>${esc(tr('flow.noBlocksFound'))}</strong><span>${esc(tr('flow.noBlocksFoundHint'))}</span></div>`;
}
@@ -443,11 +443,11 @@ function renderFlowEditor() {
const nodesHost = $('#flowNodes');
nodesHost.innerHTML = draft.nodes.map(node => {
const meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' };
return `<article class="flow-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
<button class="flow-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
return `<article class="diagram-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
<button class="diagram-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div>
<div class="flow-node-body">${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `<div class="flow-node-current" data-flow-shared-current="${esc(node.config.input_id)}"><span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>—</strong></div>` : ''}</div>
<button class="flow-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
<button class="diagram-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
</article>`;
}).join('');
$('#flowEmptyHint').hidden = draft.nodes.length > 0;
+3 -3
View File
@@ -66,7 +66,7 @@ function showFormError(form, message) {
summary.className = 'inline-alert error form-error-summary';
summary.setAttribute('role', 'alert');
summary.innerHTML = `<span>${esc(tr('devices.lastError'))}</span><strong>${esc(message)}</strong>`;
const anchor = $('.settings-save-bar', form) || $('.form-actions', form);
const anchor = $('.sticky-form-actions', form) || $('.form-actions', form);
if (anchor?.parentElement) anchor.parentElement.insertBefore(summary, anchor);
else form.appendChild(summary);
}
@@ -259,7 +259,7 @@ function confirmDiscardForm(form) {
function activeDirtySettingsForm() {
const active = $('.view.active');
if (!active) return null;
const form = $('form.settings-form', active);
const form = $('form.config-form', active);
return form && isFormDirty(form) ? form : null;
}
@@ -273,7 +273,7 @@ function requestDialogClose(dialog) {
if (!dialog) return true;
const form = $('form', dialog);
if (form && !confirmDiscardForm(form)) return false;
closeChartPreview(dialog.querySelector?.('.history-chart-card.chart-fullscreen-fallback'));
closeChartPreview(dialog.querySelector?.('.chart-card.chart-fullscreen-fallback'));
dialog.close();
return true;
}
+38 -4
View File
@@ -272,17 +272,31 @@ function persistSavedCharts() {
localStorage.setItem('gree_controller_saved_charts', JSON.stringify(app.savedCharts.slice(0, 30)));
}
function savedChartRangeLabel(hours) {
const value = Number(hours || 24);
if (value === 168) return tr('history.range7d');
if (value === 720) return tr('history.range30d');
if (value === 2160) return tr('history.range90d');
if (value === 8760) return tr('history.range1y');
return `${value} h`;
}
function renderCustomBuilder() {
const host = $('#historyCustomBuilder'); if (!host) return;
if (app.historyTab !== 'custom') { host.innerHTML = ''; host.classList.remove('active'); return; }
host.classList.add('active');
const options = customSeriesOptions();
const selected = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
const editing = app.savedCharts.find(item => item.id === app.customChartEditingId) || null;
const draftName = app.customChartNameDraft !== null ? app.customChartNameDraft : (editing?.name || '');
const editNote = editing ? `<span class="custom-chart-edit-note">${esc(tr('history.editingChart'))}: <strong>${esc(editing.name)}</strong></span>` : '';
const cancelEdit = editing ? `<button class="secondary" data-history-action="cancel-chart-edit">${esc(tr('actions.cancel'))}</button>` : '';
host.innerHTML = `<div class="panel custom-chart-panel"><div class="chart-title-row"><div><h3>${esc(tr('history.customTitle'))}</h3><p>${esc(tr('history.customDescription'))}</p></div></div>
<div class="custom-chart-add"><select id="customSeriesSelect">${options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('')}</select><button class="secondary" data-history-action="add-series">${esc(tr('history.addSeries'))}</button></div>
<div class="custom-series-list">${selected.length ? selected.map((item, index) => `<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="${esc(tr('actions.remove'))}">${uiIcon('close')}</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
<div class="custom-chart-save"><input id="customChartName" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(tr('actions.save'))}</button><button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
<div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">${uiIcon('close')}</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
${editNote}
<div class="custom-chart-save"><input id="customChartName" value="${esc(draftName)}" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(editing ? tr('history.saveChanges') : tr('actions.save'))}</button>${cancelEdit}<button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
<div class="saved-chart-section"><div class="saved-chart-heading"><strong>${esc(tr('history.savedCharts'))}</strong><small>${app.savedCharts.length}</small></div><div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row${item.id === app.customChartEditingId ? ' is-editing' : ''}"><button class="saved-chart-open" data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))} · ${esc(savedChartRangeLabel(item.hours))}</small></button><div class="saved-chart-actions"><button class="secondary" data-history-action="edit-chart" data-id="${esc(item.id)}" title="${esc(tr('actions.edit'))}" aria-label="${esc(tr('actions.edit'))}">${uiIcon('edit')}</button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}" title="${esc(tr('actions.delete'))}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div></div>
</div>`;
}
@@ -327,26 +341,46 @@ function publicCustomChartUrl(path) {
async function handleHistoryAction(button) {
const action = button.dataset.historyAction;
if (action === 'add-series') {
if ($('#customChartName')) app.customChartNameDraft = $('#customChartName').value;
const value = $('#customSeriesSelect')?.value;
if (value && !app.customChartSeries.includes(value)) app.customChartSeries.push(value);
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
}
if (action === 'remove-series') {
if ($('#customChartName')) app.customChartNameDraft = $('#customChartName').value;
app.customChartSeries.splice(Number(button.dataset.index), 1);
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
}
if (action === 'save-chart') {
if (!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
const name = $('#customChartName')?.value.trim() || tr('history.customChart');
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours: $('#historyHours')?.value || '24' };
app.savedCharts.unshift(item); persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
const hours = $('#historyHours')?.value || '24';
const editingIndex = app.savedCharts.findIndex(entry => entry.id === app.customChartEditingId);
if (editingIndex >= 0) {
app.savedCharts[editingIndex] = { ...app.savedCharts[editingIndex], name, series: [...app.customChartSeries], hours };
app.customChartEditingId = null; app.customChartNameDraft = null;
persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartUpdated')); return;
}
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours };
app.savedCharts.unshift(item); app.customChartNameDraft = null; persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
}
if (action === 'load-chart') {
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
app.customChartEditingId = null; app.customChartNameDraft = item.name || '';
app.customChartSeries = [...item.series]; if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); return;
}
if (action === 'edit-chart') {
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
app.customChartEditingId = item.id; app.customChartNameDraft = item.name || ''; app.customChartSeries = [...item.series];
if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); $('#customChartName')?.focus(); return;
}
if (action === 'cancel-chart-edit') {
app.customChartEditingId = null; app.customChartNameDraft = null; renderCustomHistory(); return;
}
if (action === 'delete-chart') {
if (app.customChartEditingId === button.dataset.id) { app.customChartEditingId = null; app.customChartNameDraft = null; }
app.savedCharts = app.savedCharts.filter(entry => entry.id !== button.dataset.id); persistSavedCharts(); renderCustomHistory(); return;
}
if (action === 'copy-chart-link') {
+170 -138
View File
@@ -736,7 +736,7 @@ h3 {
padding: 7px 11px;
}
.zone-enable-toggle {
.enable-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -749,13 +749,13 @@ h3 {
font-size: 11px;
}
.zone-enable-toggle.active {
.enable-toggle.active {
border-color: color-mix(in srgb, var(--accent) 34%, var(--line));
color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
.zone-enable-toggle span {
.enable-toggle span {
font-size: 12px;
font-weight: 900;
}
@@ -1639,16 +1639,16 @@ button.outside-pill:focus-visible {
font-variant-numeric: tabular-nums;
}
.outdoor-history-dialog .history-chart-card {
.outdoor-history-dialog .chart-card {
margin: 0;
}
.outdoor-history-dialog .history-chart-card:not(.chart-fullscreen-fallback) .chart-wrap {
.outdoor-history-dialog .chart-card:not(.chart-fullscreen-fallback) .chart-wrap {
height: auto;
max-height: none;
}
.outdoor-history-dialog .history-chart-card:not(.chart-fullscreen-fallback) canvas {
.outdoor-history-dialog .chart-card:not(.chart-fullscreen-fallback) canvas {
height: 350px;
}
@@ -2031,7 +2031,7 @@ button.outside-pill:focus-visible {
line-height: 1.5;
}
.history-chart-card {
.chart-card {
margin-top: 14px;
}
@@ -2188,35 +2188,6 @@ button.outside-pill:focus-visible {
}
}
.history-tabs {
display: flex;
gap: 7px;
overflow-x: auto;
margin: 0 0 14px;
padding: 3px;
scrollbar-width: none;
}
.history-tabs::-webkit-scrollbar {
display: none;
}
.history-tabs button {
flex: 0 0 auto;
padding: 9px 13px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
background: var(--surface);
font-size: 12px;
font-weight: 700;
}
.history-tabs button.active {
color: var(--accent-text);
border-color: var(--accent);
background: var(--accent);
}
.history-toolbar-panel {
margin-bottom: 14px;
@@ -2309,23 +2280,49 @@ button.outside-pill:focus-visible {
line-height: 1;
}
.custom-chart-edit-note {
color: var(--muted);
font-size: 12px;
}
.custom-chart-edit-note strong { color: var(--text-soft); }
.saved-chart-section {
display: grid;
gap: 8px;
padding-top: 10px;
border-top: 1px solid var(--line);
}
.saved-chart-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.saved-chart-heading small { color: var(--muted); }
.saved-chart-list {
display: grid;
gap: 7px;
padding-top: 3px;
border-top: 1px solid var(--line);
}
.saved-chart-row {
display: grid;
grid-template-columns: 1fr auto;
grid-template-columns: minmax(0, 1fr) auto;
gap: 7px;
}
.saved-chart-row>button:first-child {
.saved-chart-row.is-editing .saved-chart-open {
border-color: color-mix(in srgb, var(--accent) 65%, var(--line));
}
.saved-chart-open {
display: flex;
justify-content: space-between;
align-items: center;
min-width: 0;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--line);
@@ -2344,7 +2341,12 @@ button.outside-pill:focus-visible {
color: var(--muted);
}
.saved-chart-row>.danger {
.saved-chart-actions {
display: flex;
gap: 7px;
}
.saved-chart-actions button {
width: 42px;
padding: 0;
border: 1px solid var(--line);
@@ -2679,7 +2681,7 @@ button.outside-pill:focus-visible {
}
}
.history-chart-card canvas {
.chart-card canvas {
width: 100%;
min-width: 720px;
}
@@ -2960,7 +2962,7 @@ button.outside-pill:focus-visible {
align-self: end;
}
.settings-form {
.config-form {
display: grid;
gap: 14px;
}
@@ -3001,39 +3003,39 @@ button.outside-pill:focus-visible {
display: none;
}
.settings-block {
.form-section {
display: grid;
gap: 18px;
}
.settings-block-head {
.form-section-head {
display: flex;
align-items: flex-start;
}
.settings-block-head h3 {
.form-section-head h3 {
margin: 1px 0 4px;
font-size: 18px;
}
.settings-block-head p {
.form-section-head p {
margin: 0;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.settings-grid {
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.settings-grid .wide {
.form-grid .wide {
grid-column: 1/-1;
}
.settings-save-bar {
.sticky-form-actions {
position: sticky;
bottom: 18px;
z-index: 9;
@@ -3049,7 +3051,7 @@ button.outside-pill:focus-visible {
backdrop-filter: blur(16px);
}
.settings-save-bar span {
.sticky-form-actions span {
color: var(--muted);
font-size: 12px;
}
@@ -3240,7 +3242,7 @@ button.outside-pill:focus-visible {
font-size: 12px;
}
.flow-node {
.diagram-node {
position: absolute;
z-index: 3;
display: grid;
@@ -3253,31 +3255,31 @@ button.outside-pill:focus-visible {
box-shadow: 0 8px 24px rgba(0, 0, 0, .15);
}
.flow-node.input {
.diagram-node.input {
border-left: 3px solid var(--teal);
}
.flow-node.logic {
.diagram-node.logic {
border-left: 3px solid var(--purple);
}
.flow-node.action {
.diagram-node.action {
border-left: 3px solid var(--accent);
}
.flow-node.event {
.diagram-node.event {
border-left: 3px solid var(--info);
}
.flow-node.decision.demand {
.diagram-node.decision.demand {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent), 0 8px 24px rgba(0, 0, 0, .15);
}
.flow-node.global {
.diagram-node.global {
min-height: 106px;
}
.flow-node.night-active {
.diagram-node.night-active {
border-color: color-mix(in srgb, var(--info) 45%, var(--line-strong));
}
@@ -3293,7 +3295,7 @@ button.outside-pill:focus-visible {
text-transform: uppercase;
}
.flow-node h3 {
.diagram-node h3 {
overflow: hidden;
margin: 0;
color: var(--text);
@@ -3302,13 +3304,13 @@ button.outside-pill:focus-visible {
white-space: nowrap;
}
.flow-node>strong {
.diagram-node>strong {
color: var(--text);
font-size: 17px;
line-height: 1.25;
}
.flow-node p {
.diagram-node p {
display: -webkit-box;
overflow: hidden;
margin: 0;
@@ -3336,7 +3338,7 @@ button.outside-pill:focus-visible {
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
.flow-port {
.diagram-port {
position: absolute;
top: 50%;
width: 9px;
@@ -3347,27 +3349,27 @@ button.outside-pill:focus-visible {
background: var(--muted);
}
.flow-port.in {
.diagram-port.in {
left: -6px;
}
.flow-port.out {
.diagram-port.out {
right: -6px;
}
.flow-node.input .flow-port {
.diagram-node.input .diagram-port {
background: var(--teal);
}
.flow-node.logic .flow-port {
.diagram-node.logic .diagram-port {
background: var(--purple);
}
.flow-node.action .flow-port {
.diagram-node.action .diagram-port {
background: var(--accent);
}
.flow-node.event .flow-port {
.diagram-node.event .diagram-port {
background: var(--info);
}
@@ -3384,15 +3386,15 @@ button.outside-pill:focus-visible {
grid-column: auto;
}
.settings-grid {
.form-grid {
grid-template-columns: 1fr;
}
.settings-grid .wide {
.form-grid .wide {
grid-column: auto;
}
.settings-save-bar {
.sticky-form-actions {
bottom: 76px;
}
@@ -3416,7 +3418,7 @@ button.outside-pill:focus-visible {
}
}
.settings-block-head {
.form-section-head {
gap: 0;
}
@@ -3480,7 +3482,7 @@ button.outside-pill:focus-visible {
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}
.standalone-settings-form {
.full-width-form {
width: 100%;
max-width: none;
}
@@ -4135,7 +4137,7 @@ body.simulation-standalone [data-view="simulation"] {
}
.zone-config-card .zone-enable-toggle {
.zone-config-card .enable-toggle {
min-height: 28px;
padding: 4px 8px;
font-size: 10px;
@@ -4588,7 +4590,7 @@ body.simulation-standalone [data-view="simulation"] {
font-size: 11px;
}
.quick-thermostat-control .zone-enable-toggle {
.quick-thermostat-control .enable-toggle {
flex: 0 0 auto;
}
@@ -5326,7 +5328,7 @@ textarea[aria-invalid="true"] {
}
.settings-form>.form-error-summary {
.config-form>.form-error-summary {
margin: 0 0 12px;
}
@@ -5334,7 +5336,7 @@ textarea[aria-invalid="true"] {
margin-top: -2px;
}
.settings-form.is-saving,
.config-form.is-saving,
.dialog-form.is-saving {
cursor: progress;
}
@@ -5417,7 +5419,7 @@ textarea[aria-invalid="true"] {
}
@media (hover:hover) and (pointer:fine) {
.history-chart-card .chart-wrap canvas {
.chart-card .chart-wrap canvas {
cursor: crosshair;
}
}
@@ -5492,7 +5494,7 @@ textarea[aria-invalid="true"] {
}
}
.settings-form.has-unsaved-changes .settings-save-bar {
.config-form.has-unsaved-changes .sticky-form-actions {
border-color: color-mix(in srgb, var(--accent) 48%, var(--line-strong));
box-shadow: 0 8px 30px rgba(0, 0, 0, .18), 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent);
}
@@ -6172,7 +6174,7 @@ textarea[aria-invalid="true"] {
stroke-width: 4;
}
.flow-editor .flow-node {
.flow-editor .diagram-node {
position: absolute;
z-index: 2;
display: block;
@@ -6188,7 +6190,7 @@ textarea[aria-invalid="true"] {
user-select: none;
}
.flow-editor .flow-node.selected {
.flow-editor .diagram-node.selected {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-soft), 0 12px 30px rgba(0, 0, 0, .22);
}
@@ -6236,7 +6238,7 @@ textarea[aria-invalid="true"] {
overflow-wrap: anywhere;
}
.flow-editor .flow-port {
.flow-editor .diagram-port {
position: absolute;
top: 50%;
border-radius: 50%;
@@ -6247,8 +6249,8 @@ textarea[aria-invalid="true"] {
}
.flow-editor .flow-port:hover,
.flow-editor .flow-port.armed {
.flow-editor .diagram-port:hover,
.flow-editor .diagram-port.armed {
background: var(--accent);
transform: scale(1.15);
}
@@ -7230,31 +7232,31 @@ textarea[aria-invalid="true"] {
line-height: 1.15;
}
.flow-palette-trigger {
.block-category-trigger {
border-left-color: var(--warning);
}
.flow-palette-time {
.block-category-time {
border-left-color: var(--blue);
}
.flow-palette-timeop {
.block-category-timeop {
border-left-color: var(--info);
}
.flow-palette-sensor {
.block-category-sensor {
border-left-color: var(--teal);
}
.flow-palette-logic {
.block-category-logic {
border-left-color: var(--purple);
}
.flow-palette-action {
.block-category-action {
border-left-color: var(--orange);
}
.flow-palette-haaction {
.block-category-haaction {
border-left-color: var(--accent);
}
@@ -7380,7 +7382,7 @@ button {
margin-bottom: 0;
}
.automation-tabs {
.segmented-tabs {
display: flex;
align-items: center;
gap: 5px;
@@ -7395,11 +7397,11 @@ button {
scrollbar-width: none;
}
.automation-tabs::-webkit-scrollbar {
.segmented-tabs::-webkit-scrollbar {
display: none;
}
.automation-tabs button {
.segmented-tabs button {
flex: 0 0 auto;
min-height: 38px;
padding: 8px 12px;
@@ -7409,7 +7411,7 @@ button {
font-weight: 750;
}
.automation-tabs button.active {
.segmented-tabs button.active {
color: var(--accent);
background: var(--accent-soft);
box-shadow: inset 0 0 0 1px var(--accent-border);
@@ -7614,31 +7616,31 @@ button {
border-left: 3px solid var(--line-strong);
}
.flow-block-library-group.flow-palette-trigger {
.flow-block-library-group.block-category-trigger {
border-left-color: var(--warning);
}
.flow-block-library-group.flow-palette-time {
.flow-block-library-group.block-category-time {
border-left-color: var(--blue);
}
.flow-block-library-group.flow-palette-timeop {
.flow-block-library-group.block-category-timeop {
border-left-color: var(--info);
}
.flow-block-library-group.flow-palette-sensor {
.flow-block-library-group.block-category-sensor {
border-left-color: var(--teal);
}
.flow-block-library-group.flow-palette-logic {
.flow-block-library-group.block-category-logic {
border-left-color: var(--purple);
}
.flow-block-library-group.flow-palette-action {
.flow-block-library-group.block-category-action {
border-left-color: var(--orange);
}
.flow-block-library-group.flow-palette-haaction {
.flow-block-library-group.block-category-haaction {
border-left-color: var(--accent);
}
@@ -7661,7 +7663,7 @@ button {
text-align: left;
}
.flow-editor .flow-port {
.flow-editor .diagram-port {
width: 16px;
height: 16px;
margin-top: -8px;
@@ -7675,7 +7677,7 @@ button {
right: -9px;
}
.flow-editor .flow-port::after {
.flow-editor .diagram-port::after {
content: "";
position: absolute;
inset: -11px;
@@ -7822,12 +7824,12 @@ button {
min-height: 44px;
}
.automation-tabs {
.segmented-tabs {
margin-bottom: 12px;
scroll-snap-type: x proximity;
}
.automation-tabs button {
.segmented-tabs button {
flex: 1 0 auto;
scroll-snap-align: start;
}
@@ -7917,17 +7919,6 @@ button {
min-height: 44px;
}
.history-tabs {
margin-inline: -12px;
padding-inline: 12px;
scroll-padding-inline: 12px;
scroll-snap-type: x proximity;
}
.history-tabs button {
min-height: 40px;
scroll-snap-align: start;
}
.history-toolbar-panel {
padding: 12px;
@@ -7942,17 +7933,17 @@ button {
backdrop-filter: blur(12px);
}
.settings-save-bar {
.sticky-form-actions {
bottom: calc(74px + env(safe-area-inset-bottom));
margin-inline: -4px;
padding: 10px;
}
.settings-save-bar span {
.sticky-form-actions span {
display: none;
}
.settings-save-bar button {
.sticky-form-actions button {
width: 100%;
min-height: 44px;
}
@@ -8332,15 +8323,15 @@ button {
grid-column: 2;
}
.settings-block {
.form-section {
gap: 14px;
}
.settings-block-head h3 {
.form-section-head h3 {
font-size: 16px;
}
.settings-block-head p,
.form-section-head p,
.field-note {
line-height: 1.45;
}
@@ -8531,7 +8522,7 @@ body.flow-editor-open {
overflow: hidden;
}
.history-chart-card.chart-fullscreen-fallback {
.chart-card.chart-fullscreen-fallback {
position: fixed;
inset: 0;
z-index: 140;
@@ -8551,16 +8542,16 @@ body.flow-editor-open {
box-shadow: none;
}
.history-chart-card.chart-fullscreen-fallback .chart-title-row {
.chart-card.chart-fullscreen-fallback .chart-title-row {
margin: 0;
}
.history-chart-card.chart-fullscreen-fallback .chart-title-row p,
.history-chart-card.chart-fullscreen-fallback .chart-hover-hint {
.chart-card.chart-fullscreen-fallback .chart-title-row p,
.chart-card.chart-fullscreen-fallback .chart-hover-hint {
display: none;
}
.history-chart-card.chart-fullscreen-fallback .chart-wrap {
.chart-card.chart-fullscreen-fallback .chart-wrap {
height: 100%;
min-height: 0;
overflow: auto;
@@ -8569,7 +8560,7 @@ body.flow-editor-open {
background: var(--surface-muted);
}
.history-chart-card.chart-fullscreen-fallback .legend {
.chart-card.chart-fullscreen-fallback .legend {
max-height: 88px;
margin: 0;
overflow: auto;
@@ -8611,7 +8602,7 @@ body.flow-editor-open {
text-align: left;
}
.automation-tabs {
.segmented-tabs {
width: 100%;
min-width: 0;
}
@@ -9981,35 +9972,73 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
/* Standalone public custom chart */
.public-custom-chart-page {
min-height: 100vh;
height: 100vh;
height: 100dvh;
min-height: 0;
overflow: hidden;
}
.public-custom-chart-shell {
display: flex;
width: 100%;
min-height: 100vh;
height: 100vh;
height: 100dvh;
min-height: 0;
padding: 16px;
overflow: hidden;
}
.public-custom-chart-card {
display: flex;
min-height: calc(100vh - 32px);
width: 100%;
height: 100%;
min-height: 0;
margin: 0;
flex-direction: column;
overflow: hidden;
}
.public-chart-title-row {
flex: 0 0 auto;
align-items: end;
}
.public-chart-heading { min-width: 0; }
.public-chart-range-control {
display: grid;
flex: 0 0 auto;
gap: 6px;
color: var(--muted);
font-size: 11px;
}
.public-chart-range-control select { min-width: 126px; }
.public-custom-chart-wrap {
flex: 1 1 auto;
min-height: 360px;
flex: 1 1 0;
min-height: 180px;
overflow-x: auto;
overflow-y: hidden;
}
#publicCustomChart {
width: 100%;
min-width: 720px;
height: 100%;
min-height: 420px;
min-height: 0;
}
.public-custom-chart-card .legend {
flex: 0 0 auto;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
.public-custom-chart-card .legend::-webkit-scrollbar { display: none; }
.public-chart-legend-item {
cursor: default;
}
@@ -10028,5 +10057,8 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
@media (max-width: 700px) {
.public-custom-chart-shell { padding: 8px; }
.public-custom-chart-card { min-height: calc(100vh - 16px); padding: 14px; }
.public-custom-chart-card { padding: 12px; }
.public-chart-title-row { align-items: center; }
.public-chart-range-control select { min-width: 108px; }
.public-custom-chart-wrap { min-height: 140px; }
}