v0.14.11
This commit is contained in:
+63
-62
@@ -24,15 +24,15 @@
|
||||
<div class="top-actions">
|
||||
<div class="toolbar-picker" data-i18n-title="controls.language" title="Language">
|
||||
<span class="toolbar-picker-value" id="languageLabel" aria-hidden="true">EN</span>
|
||||
<span class="toolbar-picker-chevron" aria-hidden="true">⌄</span>
|
||||
<span class="toolbar-picker-chevron" data-ui-icon="chevron-down" aria-hidden="true"></span>
|
||||
<select class="toolbar-picker-select" id="languageSelect" data-i18n-aria="controls.language"
|
||||
aria-label="Language">
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-picker" data-i18n-title="controls.theme" title="Theme">
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" aria-hidden="true">◐</span>
|
||||
<span class="toolbar-picker-chevron" aria-hidden="true">⌄</span>
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" data-ui-icon="system" aria-hidden="true"></span>
|
||||
<span class="toolbar-picker-chevron" data-ui-icon="chevron-down" aria-hidden="true"></span>
|
||||
<select class="toolbar-picker-select" id="themeSelect" data-i18n-aria="controls.theme" aria-label="Theme">
|
||||
<option value="system" data-i18n="theme.system">System</option>
|
||||
<option value="light" data-i18n="theme.light">Light</option>
|
||||
@@ -40,7 +40,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<button class="icon-button toolbar-icon-control" id="refreshButton" data-i18n-title="actions.refresh"
|
||||
data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh">↻</button>
|
||||
data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh" data-ui-icon="refresh"></button>
|
||||
</div>
|
||||
<div class="connection-banner" id="connectionBanner" role="status" aria-live="polite" hidden>
|
||||
<span class="connection-banner-dot" aria-hidden="true"></span>
|
||||
@@ -844,14 +844,14 @@
|
||||
<section id="flowEditor" class="flow-editor" hidden aria-label="Flow editor">
|
||||
<header class="flow-editor-bar">
|
||||
<div class="flow-editor-title"><button type="button" class="icon-button" data-action="close-flow-editor"
|
||||
aria-label="Back">←</button>
|
||||
aria-label="Back" data-ui-icon="arrow-left"></button>
|
||||
<div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span>
|
||||
<div class="flow-name-wrap">
|
||||
<div id="flowNameView" class="flow-name-view" hidden><button id="flowNameText" class="flow-name-text"
|
||||
type="button" data-action="edit-flow-name" data-i18n-title="flow.editName"
|
||||
title="Edit Flow name"></button><button class="flow-name-edit" type="button"
|
||||
data-action="edit-flow-name" data-i18n-title="flow.editName" data-i18n-aria="flow.editName"
|
||||
title="Edit Flow name" aria-label="Edit Flow name">✎</button></div><input id="flowName" maxlength="100"
|
||||
title="Edit Flow name" aria-label="Edit Flow name" data-ui-icon="edit"></button></div><input id="flowName" maxlength="100"
|
||||
data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint"
|
||||
placeholder="My automation">
|
||||
</div>
|
||||
@@ -929,7 +929,7 @@
|
||||
<div class="flow-workspace-wrap">
|
||||
<div class="flow-workspace-toolbar">
|
||||
<div class="flow-workspace-primary-actions"><button type="button" class="primary flow-add-block-button"
|
||||
data-action="flow-add-block"><span aria-hidden="true">+</span><span data-i18n="flow.addBlock">Add
|
||||
data-action="flow-add-block"><span data-ui-icon="plus" aria-hidden="true"></span><span data-i18n="flow.addBlock">Add
|
||||
block</span></button><button type="button" class="secondary flow-selection-action"
|
||||
data-action="flow-select-all" data-i18n="flow.selectAll">Zaznacz wszystko</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-clear-selection"
|
||||
@@ -937,11 +937,12 @@
|
||||
class="badge"></span></div>
|
||||
<div class="flow-zoom-controls" role="group" data-i18n-aria="flow.zoomControls" aria-label="Canvas zoom">
|
||||
<button type="button" class="secondary icon-button" data-action="flow-zoom-out"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out">−</button><span id="flowZoomLabel"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out" data-ui-icon="minus"></button><span id="flowZoomLabel"
|
||||
aria-live="polite">100%</span><button type="button" class="secondary icon-button"
|
||||
data-action="flow-zoom-in" data-i18n-title="flow.zoomIn" title="Zoom in">+</button><button type="button"
|
||||
class="secondary flow-fit-button" data-action="flow-fit" data-i18n="flow.fitView"
|
||||
data-i18n-title="flow.fitView" title="Fit">Fit</button>
|
||||
data-action="flow-zoom-in" data-i18n-title="flow.zoomIn" title="Zoom in" data-ui-icon="plus"></button><button type="button"
|
||||
class="secondary flow-fit-button" data-action="flow-fit" data-i18n-title="flow.fitView" title="Fit"><span
|
||||
class="flow-fit-button-icon" data-ui-icon="fit" aria-hidden="true"></span><span
|
||||
class="flow-fit-button-label" data-i18n="flow.fitView">Fit</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="flowWorkspace" class="flow-workspace" tabindex="0">
|
||||
@@ -956,7 +957,7 @@
|
||||
<div class="flow-natural-preview" id="flowNaturalPreview">
|
||||
<button type="button" class="flow-preview-toggle" data-action="flow-preview-toggle"
|
||||
aria-expanded="false"><strong data-i18n="flow.previewDetails">Interpretation and Flow cycle</strong><span
|
||||
class="flow-preview-toggle-icon" aria-hidden="true">⌄</span></button>
|
||||
class="flow-preview-toggle-icon" data-ui-icon="chevron-down" aria-hidden="true"></span></button>
|
||||
<div class="flow-preview-details">
|
||||
<div class="flow-preview-item flow-preview-interpretation"><strong
|
||||
data-i18n="flow.interpretation">Interpretation</strong><span id="flowInterpretation"></span></div>
|
||||
@@ -976,7 +977,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.blocks">Blocks</span>
|
||||
<h2 data-i18n="flow.addBlock">Add block</h2>
|
||||
</div><button type="button" data-close aria-label="Close">×</button>
|
||||
</div><button type="button" data-close aria-label="Close" data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="flow-block-search"><input id="flowBlockSearch" type="search" autocomplete="off"
|
||||
data-i18n-placeholder="flow.searchBlocks" placeholder="Search blocks…"></div>
|
||||
@@ -985,16 +986,16 @@
|
||||
|
||||
<dialog id="flowEditorActionsDialog" class="sheet flow-editor-actions-dialog">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="flow.actions">Actions</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="flow.actions">Actions</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<button type="button" data-action="flow-templates"><span
|
||||
data-i18n="flow.templates">Templates</span><span>›</span></button>
|
||||
<button type="button" data-action="import-flow"><span data-i18n="flow.import">Import</span><span>›</span></button>
|
||||
<button type="button" data-action="export-flow"><span data-i18n="flow.export">Export</span><span>›</span></button>
|
||||
data-i18n="flow.templates">Templates</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="import-flow"><span data-i18n="flow.import">Import</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="export-flow"><span data-i18n="flow.export">Export</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-dry-run"><span
|
||||
data-i18n="flow.dryRun">Dry-run</span><span>›</span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span>›</span></button>
|
||||
data-i18n="flow.dryRun">Dry-run</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1005,7 +1006,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.sharedInputEyebrow">Wspólne dane</span>
|
||||
<h2 id="flowSharedInputDialogTitle" data-i18n="flow.sharedInputTitle">Wspólne wejście Flow</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<input type="hidden" name="id">
|
||||
<label><span data-i18n="common.name">Nazwa</span><input name="name" maxlength="100" required
|
||||
@@ -1034,7 +1035,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.templateLibrary">Flow library</span>
|
||||
<h2 data-i18n="flow.templates">Templates</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="flow.templatesHint">Ready-made layouts create an editable graph using existing
|
||||
zones, devices and groups.</p>
|
||||
@@ -1060,7 +1061,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.testMode">Flow diagnostics</span>
|
||||
<h2 id="flowTestTitle" data-i18n="flow.dryRun">Dry-run</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div id="flowSimulationControls" class="flow-simulation-controls">
|
||||
<p class="field-note" data-i18n="flow.dryRunHint">Dry-run does not change thermostat, device, group, schedule or
|
||||
@@ -1081,53 +1082,53 @@
|
||||
|
||||
<nav class="bottom-nav" data-i18n-aria="nav.navigation" aria-label="Navigation">
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionControl">Sterowanie</span>
|
||||
<button class="active" data-nav="dashboard"><span>⌂</span><b data-i18n="nav.dashboard">Dashboard</b></button>
|
||||
<button data-nav="zones"><span>◎</span><b data-i18n="nav.zones">Zones</b></button>
|
||||
<button class="desktop-nav-only" data-nav="groups"><span>◉</span><b data-i18n="nav.groups">Groups</b></button>
|
||||
<button class="active" data-nav="dashboard"><span data-ui-icon="home" aria-hidden="true"></span><b data-i18n="nav.dashboard">Dashboard</b></button>
|
||||
<button data-nav="zones"><span data-ui-icon="zones" aria-hidden="true"></span><b data-i18n="nav.zones">Zones</b></button>
|
||||
<button class="desktop-nav-only" data-nav="groups"><span data-ui-icon="groups" aria-hidden="true"></span><b data-i18n="nav.groups">Groups</b></button>
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionAutomation">Automatyzacja</span>
|
||||
<button data-nav="flows"><span>◇</span><b data-i18n="nav.flows">Flow</b></button>
|
||||
<button class="desktop-nav-only" data-nav="schedules"><span>◷</span><b
|
||||
<button data-nav="flows"><span data-ui-icon="flow" aria-hidden="true"></span><b data-i18n="nav.flows">Flow</b></button>
|
||||
<button class="desktop-nav-only" data-nav="schedules"><span data-ui-icon="schedule" aria-hidden="true"></span><b
|
||||
data-i18n="nav.schedules">Schedules</b></button>
|
||||
<button class="desktop-nav-only" data-nav="automations"><span>⌁</span><b
|
||||
<button class="desktop-nav-only" data-nav="automations"><span data-ui-icon="automation" aria-hidden="true"></span><b
|
||||
data-i18n="nav.automations">Automations</b></button>
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionData">Dane i system</span>
|
||||
<button data-nav="history"><span>⌁</span><b data-i18n="nav.history">History</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span>▣</span><b data-i18n="nav.devices">Devices</b></button>
|
||||
<button data-nav="history"><span data-ui-icon="history" aria-hidden="true"></span><b data-i18n="nav.history">History</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span data-ui-icon="devices" aria-hidden="true"></span><b data-i18n="nav.devices">Devices</b></button>
|
||||
<button class="desktop-nav-only" data-nav="homeassistant"><span>HA</span><b data-i18n="nav.homeAssistantShort">Home
|
||||
Assistant</b></button>
|
||||
<button class="desktop-nav-only" data-nav="simulation"><span>▶</span><b
|
||||
<button class="desktop-nav-only" data-nav="simulation"><span data-ui-icon="play" aria-hidden="true"></span><b
|
||||
data-i18n="nav.simulation">Simulator</b></button>
|
||||
<button class="desktop-nav-only" data-nav="night"><span>☾</span><b data-i18n="nav.nightMode">Night mode</b></button>
|
||||
<button class="desktop-nav-only" data-nav="logs"><span>≡</span><b data-i18n="nav.logs">Events</b></button>
|
||||
<button class="desktop-nav-only" data-nav="settings"><span>⚙</span><b data-i18n="nav.settings">Settings</b></button>
|
||||
<button class="mobile-nav-only" data-nav="more"><span>•••</span><b data-i18n="nav.more">More</b></button>
|
||||
<button class="desktop-nav-only" data-nav="night"><span data-ui-icon="moon" aria-hidden="true"></span><b data-i18n="nav.nightMode">Night mode</b></button>
|
||||
<button class="desktop-nav-only" data-nav="logs"><span data-ui-icon="events" aria-hidden="true"></span><b data-i18n="nav.logs">Events</b></button>
|
||||
<button class="desktop-nav-only" data-nav="settings"><span data-ui-icon="settings" aria-hidden="true"></span><b data-i18n="nav.settings">Settings</b></button>
|
||||
<button class="mobile-nav-only" data-nav="more"><span data-ui-icon="more-horizontal" aria-hidden="true"></span><b data-i18n="nav.more">More</b></button>
|
||||
</nav>
|
||||
|
||||
<dialog id="moreDialog" class="sheet">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="nav.more">More</h2><button data-close>×</button>
|
||||
<h2 data-i18n="nav.more">More</h2><button data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="menu-list more-menu-list">
|
||||
<span class="menu-section-label" data-i18n="nav.sectionControl">Sterowanie</span>
|
||||
<button data-go="devices"><span data-i18n="nav.devices">Devices</span><span>›</span></button>
|
||||
<button data-go="groups"><span data-i18n="nav.groups">Groups</span><span>›</span></button>
|
||||
<button data-go="devices"><span data-i18n="nav.devices">Devices</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="groups"><span data-i18n="nav.groups">Groups</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<span class="menu-section-label" data-i18n="nav.sectionAutomation">Automatyzacja</span>
|
||||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span>›</span></button>
|
||||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span>›</span></button>
|
||||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<span class="menu-section-label" data-i18n="nav.sectionData">Dane i system</span>
|
||||
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span>›</span></button>
|
||||
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span>›</span></button>
|
||||
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="homeassistant"><span data-i18n="nav.homeAssistant">Home Assistant /
|
||||
Sensors</span><span>›</span></button>
|
||||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span>›</span></button>
|
||||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span>›</span></button>
|
||||
Sensors</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="deviceDialog">
|
||||
<form method="dialog" id="deviceForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="devices.add">Add device</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="devices.add">Add device</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
|
||||
@@ -1154,7 +1155,7 @@
|
||||
|
||||
<dialog id="cloudDiscoveryDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiscoveryTitle">Devices on account</h2></div><button type="button" data-close>×</button></div>
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiscoveryTitle">Devices on account</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiscoveryHint">Devices are loaded from the saved GREE Cloud account. Nothing is added automatically; choose the units you want to register.</p>
|
||||
<div id="cloudDiscoveryList" class="discovery-name-list"></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiscoveryRefresh" data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
@@ -1164,7 +1165,7 @@
|
||||
<dialog id="discoverDialog">
|
||||
<form method="dialog" id="discoverForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.help">Auto searches both AES-ECB and AES-GCM devices. Multiple passes
|
||||
improve discovery when several Wi-Fi modules answer the same broadcast.</p>
|
||||
@@ -1185,7 +1186,7 @@
|
||||
<dialog id="discoveryNamesDialog">
|
||||
<form method="dialog" id="discoveryNamesForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Give each new unit a friendly room name. The technical
|
||||
model and MAC remain available in diagnostics.</p>
|
||||
@@ -1200,7 +1201,7 @@
|
||||
<form method="dialog" id="renameDeviceForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="devices.technicalConfig">Technical configuration</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="devices.technicalConfig">Technical configuration</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="two"><label><span data-i18n="devices.ipAddress">IP address</span><input name="ip"
|
||||
@@ -1226,7 +1227,7 @@
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="devices.cloudDetails">Cloud details</span><h2 id="deviceDetailsTitle">Device details</h2></div>
|
||||
<button type="button" data-close>×</button>
|
||||
<button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="technical-device-grid" id="deviceDetailsMeta"></div>
|
||||
@@ -1249,7 +1250,7 @@
|
||||
|
||||
<dialog id="deviceGroupsDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="devices.installation">Installation</span><h2 data-i18n="devices.installations">Split / multisplit installations</h2></div><button type="button" data-close>×</button></div>
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="devices.installation">Installation</span><h2 data-i18n="devices.installations">Split / multisplit installations</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.installationsHint">Group indoor units that share one outdoor unit. Energy and outdoor temperature can use one shared source.</p>
|
||||
<div id="deviceGroupsList" class="installation-list"></div>
|
||||
<form id="deviceGroupForm" class="installation-form">
|
||||
@@ -1271,7 +1272,7 @@
|
||||
|
||||
<dialog id="cloudDiagnosticsDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiagnostics">Cloud diagnostics</h2></div><button type="button" data-close>×</button></div>
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiagnostics">Cloud diagnostics</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiagnosticsHint">Decrypted and sanitized protocol data. Passwords, tokens and cipher keys are never included.</p>
|
||||
<pre id="cloudDiagnosticsPayload" class="mono cloud-diagnostics-payload">—</pre>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiagnosticsRefresh" data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
@@ -1284,7 +1285,7 @@
|
||||
<div><span class="eyebrow" data-i18n="devices.diagnostics">Diagnostics</span>
|
||||
<h2 data-i18n="devices.pingLive">Live ping</h2>
|
||||
</div>
|
||||
<button type="button" data-close>×</button>
|
||||
<button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="devices.pingHint">Measures a minimal GREE round-trip without changing device
|
||||
state, error counters or automation ownership. The chart refreshes while monitoring is enabled.</p>
|
||||
@@ -1305,7 +1306,7 @@
|
||||
<form method="dialog" id="zoneForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="common.zone">Zone</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="common.zone">Zone</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
|
||||
@@ -1391,7 +1392,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="zones.queueEyebrow">Compressor protection</span>
|
||||
<h2 data-i18n="zones.queueTitle">Command queue</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="zones.queueHint">Commands waiting for the compressor protection window to end.
|
||||
You can cancel one task or clear the whole queue.</p>
|
||||
@@ -1411,7 +1412,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="zones.quickThermostat">Szybki termostat</span>
|
||||
<h2 data-i18n="zones.temporaryTitle">Włącz czasowo</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div id="temporaryThermostatActive" class="temporary-active-summary" hidden>
|
||||
<div><small id="temporaryThermostatActiveLabel" data-i18n="zones.temporaryActive">Aktywny tryb
|
||||
@@ -1498,7 +1499,7 @@
|
||||
<form method="dialog" id="groupForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="groups.group">Group</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="groups.group">Group</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="groups.namePlaceholder" placeholder="Upstairs"></label>
|
||||
@@ -1518,7 +1519,7 @@
|
||||
<form method="dialog" id="scheduleForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="common.schedule">Schedule</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="common.schedule">Schedule</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="placeholder.night" placeholder="Night"></label>
|
||||
@@ -1557,7 +1558,7 @@
|
||||
<form method="dialog" id="automationForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="common.automation">Automation</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="common.automation">Automation</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="copy-settings-row"><select id="automationPreset">
|
||||
<option value="" data-i18n="automations.preset">Preset…</option>
|
||||
@@ -1638,7 +1639,7 @@
|
||||
<dialog id="generatedTokenDialog" class="auth-dialog">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="settings.tokenCreated">Token created</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="settings.tokenCreated">Token created</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="settings.tokenCreatedHint">Copy this token now. It will not be shown again.</p>
|
||||
<label><span data-i18n="auth.token">Access token</span><input id="generatedAccessToken" class="mono"
|
||||
@@ -1653,7 +1654,7 @@
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="history.24h">24 hours</span><h2 data-i18n="history.allOutdoor">Outdoor temperatures</h2></div>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close">×</button>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close" data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="outdoor-history-current"><small data-i18n="house.outdoor">Outdoor</small><strong id="outdoorHistoryCurrent">--</strong></div>
|
||||
<div id="outdoorHistoryChartHost"></div>
|
||||
|
||||
+2
-2
@@ -255,7 +255,7 @@ function updateChartFullscreenButton(card) {
|
||||
const button = card.querySelector('[data-chart-fullscreen]');
|
||||
if (!button) return;
|
||||
const active = chartCardIsFullscreen(card);
|
||||
button.textContent = active ? '×' : '⛶';
|
||||
button.innerHTML = uiIcon(active ? 'close' : 'fullscreen');
|
||||
button.title = tr(active ? 'history.exitFullscreen' : 'history.fullscreen');
|
||||
button.setAttribute('aria-label', button.title);
|
||||
}
|
||||
@@ -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' : ''}>−</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' : ''}>+</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'))}">⛶</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 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>`;
|
||||
}
|
||||
|
||||
function historySeriesColor(index) {
|
||||
|
||||
+55
-2
@@ -43,6 +43,59 @@ const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
const esc = value => String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
|
||||
const UI_ICON_PATHS = Object.freeze({
|
||||
'arrow-left': '<path d="M19 12H5"/><path d="m12 19-7-7 7-7"/>',
|
||||
'automation': '<path d="M6 3v12"/><path d="M18 9v12"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="6" r="3"/><path d="M9 18h3a6 6 0 0 0 6-6V9"/>',
|
||||
'check': '<path d="m5 12 4 4L19 6"/>',
|
||||
'chevron-down': '<path d="m6 9 6 6 6-6"/>',
|
||||
'chevron-right': '<path d="m9 6 6 6-6 6"/>',
|
||||
'circle': '<circle cx="12" cy="12" r="7"/>',
|
||||
'close': '<path d="M6 6l12 12M18 6 6 18"/>',
|
||||
'devices': '<rect x="4" y="3" width="16" height="7" rx="2"/><rect x="4" y="14" width="16" height="7" rx="2"/><path d="M8 6.5h.01M8 17.5h.01"/>',
|
||||
'edit': '<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L8 18l-4 1 1-4Z"/>',
|
||||
'events': '<path d="M8 6h12M8 12h12M8 18h12"/><circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/>',
|
||||
'expand-vertical': '<path d="m8 7 4-4 4 4M12 3v18M8 17l4 4 4-4"/>',
|
||||
'fit': '<path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/><path d="m3 8 5-5M21 8l-5-5M3 16l5 5M21 16l-5 5"/>',
|
||||
'flow': '<circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M7 6h10M6.5 7.5l4.3 8.7M17.5 7.5l-4.3 8.7"/>',
|
||||
'fullscreen': '<path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/>',
|
||||
'groups': '<circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 20v-1a6 6 0 0 1 12 0v1M14 15.5a5 5 0 0 1 7 4.5"/>',
|
||||
'history': '<path d="M4 19V5"/><path d="M4 19h16"/><path d="m7 15 4-4 3 2 5-6"/>',
|
||||
'home': '<path d="m3 11 9-8 9 8"/><path d="M5 10v11h14V10M9 21v-6h6v6"/>',
|
||||
'integration': '<path d="M7 7h11l-3-3M17 17H6l3 3"/><path d="m18 7-3 3M6 17l3-3"/>',
|
||||
'menu': '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
||||
'minus': '<path d="M5 12h14"/>',
|
||||
'moon': '<path d="M20 15.5A8 8 0 0 1 8.5 4 8 8 0 1 0 20 15.5Z"/>',
|
||||
'more-horizontal': '<circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.5" fill="currentColor" stroke="none"/>',
|
||||
'play': '<path d="m8 5 11 7-11 7Z" fill="currentColor" stroke="none"/>',
|
||||
'plus': '<path d="M12 5v14M5 12h14"/>',
|
||||
'power': '<path d="M12 2.75v8.5"/><path d="M7.12 5.12a7.25 7.25 0 1 0 9.76 0"/>',
|
||||
'refresh': '<path d="M20 6v5h-5"/><path d="M19 11a7 7 0 1 0 1 5"/>',
|
||||
'schedule': '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
'settings': '<circle cx="12" cy="12" r="3"/><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.74v.5a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/>',
|
||||
'sliders': '<path d="M4 6h10M18 6h2M4 12h3M11 12h9M4 18h7M15 18h5"/><circle cx="16" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="13" cy="18" r="2"/>',
|
||||
'star': '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-3-5.6 3 1.1-6.2L3 9.6l6.2-.9Z"/>',
|
||||
'star-filled': '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-3-5.6 3 1.1-6.2L3 9.6l6.2-.9Z" fill="currentColor" stroke="none"/>',
|
||||
'status-dot': '<circle cx="12" cy="12" r="5" fill="currentColor" stroke="none"/>',
|
||||
'sun': '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.42-1.42M17.66 6.34l1.41-1.41"/>',
|
||||
'system': '<circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16Z" fill="currentColor" stroke="none"/>',
|
||||
'system-info': '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7h.01"/>',
|
||||
'zones': '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/>'
|
||||
});
|
||||
|
||||
function uiIcon(name, className = '') {
|
||||
const body = UI_ICON_PATHS[name];
|
||||
if (!body) return '';
|
||||
const classes = ['ui-icon', className].filter(Boolean).join(' ');
|
||||
return `<svg class="${esc(classes)}" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
|
||||
}
|
||||
|
||||
function hydrateUiIcons(root = document) {
|
||||
$$('[data-ui-icon]', root).forEach(node => {
|
||||
const name = node.dataset.uiIcon;
|
||||
const className = node.dataset.uiIconClass || '';
|
||||
node.innerHTML = uiIcon(name, className);
|
||||
});
|
||||
}
|
||||
const locale = () => app.locales[app.language] || app.locales[DEFAULT_LANGUAGE] || 'en-GB';
|
||||
const tr = (key, params = {}) => {
|
||||
const template = app.translations[app.language]?.[key] ?? app.translations[DEFAULT_LANGUAGE]?.[key] ?? key;
|
||||
@@ -224,8 +277,8 @@ function updateToolbarControls() {
|
||||
if (languageLabel) languageLabel.textContent = String(app.language || DEFAULT_LANGUAGE).toUpperCase();
|
||||
const themeSelect = $('#themeSelect');
|
||||
const themeIcon = $('#themeIcon');
|
||||
const icons = { system: '◐', light: '☀', dark: '☾' };
|
||||
if (themeIcon) themeIcon.textContent = icons[app.theme] || icons.system;
|
||||
const iconName = { system: 'system', light: 'sun', dark: 'moon' }[app.theme] || 'system';
|
||||
if (themeIcon) themeIcon.innerHTML = uiIcon(iconName);
|
||||
if (themeSelect) {
|
||||
themeSelect.value = app.theme;
|
||||
const label = `${tr('controls.theme')}: ${tr(`theme.${app.theme}`)}`;
|
||||
|
||||
+5
-5
@@ -377,11 +377,11 @@ 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 = '', 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 ? `<b class="flow-node-badge ${badgeClass}">${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="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 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') }));
|
||||
nodes.push(node({ left: globalNightLeft, top: globalTop, kind: `logic global ${nightOn ? 'night-active' : ''}`, eyebrow: tr('simulation.globalInput'), title: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), meta: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${fanLabel(plan.night_mode_max_fan_speed || 1)}`, badge: nightOn ? '☾' : '○', badgeClass: nightOn ? 'active' : '' }));
|
||||
nodes.push(node({ left: globalNightLeft, top: globalTop, kind: `logic global ${nightOn ? 'night-active' : ''}`, eyebrow: tr('simulation.globalInput'), title: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), meta: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${fanLabel(plan.night_mode_max_fan_speed || 1)}`, badgeIcon: nightOn ? 'moon' : 'circle', badgeClass: nightOn ? 'active' : '' }));
|
||||
|
||||
zones.forEach((planZone, index) => {
|
||||
const zone = app.zones.find(item => item.id === planZone.zone_id);
|
||||
@@ -401,12 +401,12 @@ function renderSimulationPage() {
|
||||
const eventMeta = next ? `${next.label}${next.target_temperature == null ? '' : ` · ${fmtTemp(next.target_temperature)}`}` : tr('plan.noEvents');
|
||||
|
||||
nodes.push(`<div class="flow-lane-label" style="top:${y - 27}px"><strong>${esc(planZone.zone_name)}</strong><span>${esc(simulationLaneContext(planZone))}</span></div>`);
|
||||
nodes.push(node({ left: x.sensor, top: y, kind: 'input', eyebrow: tr('simulation.stepRoom'), title: sensorName, value: fmtTemp(sim.current), meta: source, badge: '●', badgeClass: sim.current == null ? '' : 'active' }));
|
||||
nodes.push(node({ left: x.sensor, top: y, kind: 'input', eyebrow: tr('simulation.stepRoom'), title: sensorName, value: fmtTemp(sim.current), meta: source, badgeIcon: 'status-dot', badgeClass: sim.current == null ? '' : 'active' }));
|
||||
nodes.push(node({ left: x.thermostat, top: y, kind: 'logic', eyebrow: tr('simulation.thermostat'), title: planZone.zone_name, value: sim.target == null ? '—' : fmtTemp(sim.target), meta: `${zonePresetLabel(planZone.preset)} · ${tr('simulation.hysteresis', { value: sim.hysteresis.toFixed(1) })}`, badge: houseModeLabel(sim.mode) }));
|
||||
nodes.push(node({ left: x.decision, top: y, kind: `logic decision ${sim.demand ? 'demand' : 'satisfied'}`, eyebrow: tr('simulation.stepDecision'), title: status.label, value: sim.demand ? tr('simulation.callForComfort') : tr('simulation.standby'), meta: sim.reasoning, badge: sim.demand ? '▶' : '✓', badgeClass: status.badge }));
|
||||
nodes.push(node({ left: x.decision, top: y, kind: `logic decision ${sim.demand ? 'demand' : 'satisfied'}`, eyebrow: tr('simulation.stepDecision'), title: status.label, value: sim.demand ? tr('simulation.callForComfort') : tr('simulation.standby'), meta: sim.reasoning, badgeIcon: sim.demand ? 'play' : 'check', badgeClass: status.badge }));
|
||||
const sleepMeta = sim.nativeSleep ? ` · ${tr('devices.sleep')}: ${tr('common.on')}` : '';
|
||||
nodes.push(node({ left: x.unit, top: y, kind: 'action', eyebrow: tr('simulation.stepCommand'), title: planZone.device_name || tr('common.device'), value: command, meta: `${tr('devices.quiet')}: ${quietText}${sleepMeta}`, badge: sim.mode === 'off' ? tr('simulation.manual') : (sim.demand ? tr('simulation.running') : tr('simulation.idle')), badgeClass: sim.demand ? 'active' : '' }));
|
||||
nodes.push(node({ left: x.event, top: y, kind: 'event', eyebrow: tr('simulation.nextEvent'), title: eventValue, value: next?.preset ? zonePresetLabel(next.preset) : tr('simulation.schedule'), meta: eventMeta, badge: '›' }));
|
||||
nodes.push(node({ left: x.event, top: y, kind: 'event', eyebrow: tr('simulation.nextEvent'), title: eventValue, value: next?.preset ? zonePresetLabel(next.preset) : tr('simulation.schedule'), meta: eventMeta, badgeIcon: 'chevron-right' }));
|
||||
|
||||
links.push(pathBetween(x.sensor + nodeW, mid, x.thermostat, mid, 'input-link'));
|
||||
links.push(pathBetween(x.thermostat + nodeW, mid, x.decision, mid, 'logic-link'));
|
||||
|
||||
+14
-14
@@ -127,7 +127,7 @@ function manualDeviceProblem(device) {
|
||||
}
|
||||
|
||||
function powerIconMarkup() {
|
||||
return '<svg class="power-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.75v8.5"/><path d="M7.12 5.12a7.25 7.25 0 1 0 9.76 0"/></svg>';
|
||||
return uiIcon('power', 'power-icon');
|
||||
}
|
||||
|
||||
function manualLocalDeviceCard(device) {
|
||||
@@ -145,9 +145,9 @@ function manualLocalDeviceCard(device) {
|
||||
${warning}
|
||||
${manualDeviceProblem(device)}
|
||||
<div class="temperature-control">
|
||||
<button data-action="temperature" data-delta="-1" data-device="${esc(device.id)}">−</button>
|
||||
<button data-action="temperature" data-delta="-1" data-device="${esc(device.id)}">${uiIcon('minus')}</button>
|
||||
<div class="target-temp editable-target" data-temperature-kind="device" data-id="${esc(device.id)}" data-value="${Number(device.target_temperature)}" data-editable="true" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number(device.target_temperature).toFixed(1)}<small>°C</small></div>
|
||||
<button data-action="temperature" data-delta="1" data-device="${esc(device.id)}">+</button>
|
||||
<button data-action="temperature" data-delta="1" data-device="${esc(device.id)}">${uiIcon('plus')}</button>
|
||||
</div>
|
||||
<div class="current-line">${esc(tr('devices.currentTemperature'))}: <strong>${fmtTemp(device.current_temperature)}</strong>${effectiveDeviceOutdoorTemperature(device) == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(effectiveDeviceOutdoorTemperature(device))}`}</div>
|
||||
${compressorQueuePanel(managedZone)}
|
||||
@@ -184,9 +184,9 @@ function manualCloudDeviceCard(device) {
|
||||
</div>
|
||||
${warning}
|
||||
<div class="temperature-control">
|
||||
<button data-action="temperature" data-delta="${-tempStep}" data-device="${esc(device.id)}">−</button>
|
||||
<button data-action="temperature" data-delta="${-tempStep}" data-device="${esc(device.id)}">${uiIcon('minus')}</button>
|
||||
<div class="target-temp editable-target" data-temperature-kind="device" data-id="${esc(device.id)}" data-value="${Number(device.target_temperature)}" data-temp-step="${tempStep}" data-temp-min="${minTemp}" data-temp-max="${maxTemp}" data-editable="true" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number(device.target_temperature).toFixed(1)}<small>°C</small></div>
|
||||
<button data-action="temperature" data-delta="${tempStep}" data-device="${esc(device.id)}">+</button>
|
||||
<button data-action="temperature" data-delta="${tempStep}" data-device="${esc(device.id)}">${uiIcon('plus')}</button>
|
||||
</div>
|
||||
<div class="current-line">${esc(tr('devices.currentTemperature'))}: <strong>${fmtTemp(device.current_temperature)}</strong>${effectiveDeviceOutdoorTemperature(device) == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(effectiveDeviceOutdoorTemperature(device))}`}</div>
|
||||
${compressorQueuePanel(managedZone)}
|
||||
@@ -236,7 +236,7 @@ function technicalDeviceCard(device) {
|
||||
<button type="button" data-action="rename-device" data-device="${esc(device.id)}">${esc(tr('devices.technicalConfig'))}</button>
|
||||
<button type="button" data-action="energy-config" data-device="${esc(device.id)}">${esc(tr('energy.title'))}</button>
|
||||
${device.simulated || device.connection_type === 'gree_cloud' ? '' : `<button type="button" data-action="bind" data-device="${esc(device.id)}">${esc(tr('actions.bind'))}</button>`}
|
||||
<details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
<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-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
@@ -272,7 +272,7 @@ function cloudTechnicalDeviceCard(device) {
|
||||
<button type="button" data-action="poll" data-device="${esc(device.id)}">${esc(tr('devices.readStatus'))}</button>
|
||||
<button type="button" data-action="cloud-details" data-device="${esc(device.id)}">${esc(tr('devices.cloudDetails'))}</button>
|
||||
<button type="button" data-action="cloud-diagnostics" data-device="${esc(device.id)}">${esc(tr('devices.cloudDiagnostics'))}</button>
|
||||
<details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
<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-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
@@ -395,7 +395,7 @@ function renderCompressorQueueModal() {
|
||||
cancelAll.hidden = pending.length === 0;
|
||||
cancelAll.textContent = pending.length ? `${tr('zones.cancelAllQueued')} (${pending.length})` : tr('zones.cancelAllQueued');
|
||||
if (!pending.length) {
|
||||
list.innerHTML = `<div class="compressor-queue-empty"><span aria-hidden="true">✓</span><strong>${esc(tr('zones.queueEmptyTitle'))}</strong><p>${esc(tr('zones.queueEmptyText'))}</p></div>`;
|
||||
list.innerHTML = `<div class="compressor-queue-empty"><span aria-hidden="true">${uiIcon('check')}</span><strong>${esc(tr('zones.queueEmptyTitle'))}</strong><p>${esc(tr('zones.queueEmptyText'))}</p></div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = pending.map((zone, index) => {
|
||||
@@ -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>${zone.enabled ? '✓' : '○'}</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="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="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>
|
||||
@@ -551,7 +551,7 @@ function zoneCard(zone, detailed = true) {
|
||||
<div class="sensor-detail">${esc(sensorDetails)}</div>
|
||||
${compressorQueuePanel(zone)}
|
||||
${manualTakeover}
|
||||
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</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-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ function zoneCard(zone, detailed = true) {
|
||||
return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-card="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
||||
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}</p></div><div class="zone-quick-actions">${localThermostatPowerControl}${temporaryThermostatControl}</div></div>
|
||||
${disabledManualControlNotice}
|
||||
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5">−</button><div class="target-temp compact editable-target" data-temperature-kind="zone" data-id="${esc(zone.id)}" data-value="${Number.isFinite(target) ? target : ''}" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number.isFinite(target) ? target.toFixed(1) : '--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
|
||||
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5">${uiIcon('minus')}</button><div class="target-temp compact editable-target" data-temperature-kind="zone" data-id="${esc(zone.id)}" data-value="${Number.isFinite(target) ? target : ''}" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number.isFinite(target) ? target.toFixed(1) : '--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">${uiIcon('plus')}</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
|
||||
<div class="preset-row quick-control-row quick-control-row-4">
|
||||
${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${manual === preset ? 'active' : ''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset === 'sleep' ? tr('zones.sleepNow') : zonePresetLabel(preset))}</button>`).join('')}
|
||||
</div>
|
||||
@@ -635,7 +635,7 @@ function groupCard(group, detailed = false) {
|
||||
<div class="group-control-block"><small>${esc(tr('groups.mode'))}</small><div class="mode-row group-mode-row">${['house', 'heat', 'cool'].map(mode => `<button class="${state.mode === mode ? 'active' : ''}" data-action="group-mode" data-id="${esc(group.id)}" data-value="${mode}"${climateControlsDisabled}>${esc(mode === 'house' ? tr('groups.followHouse') : modeLabel(mode))}</button>`).join('')}</div></div>
|
||||
<div class="group-control-block"><small>${esc(tr('groups.profile'))}</small><div class="preset-row group-preset-row">${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${state.preset === preset ? 'active' : ''}" data-action="group-preset" data-id="${esc(group.id)}" data-value="${preset}"${climateControlsDisabled}>${esc(zonePresetLabel(preset))}</button>`).join('')}<button class="${state.preset === 'custom' ? 'active' : (customDraft?.open === true ? 'editing' : '')}" data-action="group-custom-open" data-id="${esc(group.id)}" title="${esc(tr('groups.customOpenHint'))}"${climateControlsDisabled}>${esc(tr('preset.custom'))}</button></div>
|
||||
<div class="group-custom-temperature" data-group-custom-editor="${esc(group.id)}" ${customEditorVisible ? '' : 'hidden'}><label><span>${esc(tr('groups.customTemperature'))}</span><div class="group-custom-temperature-row"><input type="text" inputmode="decimal" min="8" max="30" step="0.1" value="${customTarget.toFixed(1)}" data-group-custom-temperature="${esc(group.id)}" aria-label="${esc(tr('groups.customTemperature'))}" title="${esc(tr('groups.customTemperatureHint'))}"><span>°C</span><button type="button" class="primary" data-action="group-custom-temperature" data-id="${esc(group.id)}" title="${esc(tr('groups.applyCustomTemperatureHint'))}">${esc(tr('actions.apply'))}</button><button type="button" class="secondary" data-action="group-custom-cancel" data-id="${esc(group.id)}">${esc(tr('actions.cancel'))}</button></div></label>${customDraftPending ? `<small class="group-custom-draft-note">${esc(tr('groups.customDraftPending'))}</small>` : ''}</div></div>
|
||||
${detailed ? `<div class="card-footer"><small>${esc(tr('groups.memberCount', { count: state.zones.length }))}</small><div class="card-menu"><button data-action="edit-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>` : ''}
|
||||
${detailed ? `<div class="card-footer"><small>${esc(tr('groups.memberCount', { count: state.zones.length }))}</small><div class="card-menu"><button data-action="edit-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</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-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>` : ''}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -700,7 +700,7 @@ function renderSchedules() {
|
||||
const days = item.weekdays.map(day => dayNames[day - 1]).join(', ');
|
||||
return `<article class="list-card ${item.flow_id ? 'flow-generated-card' : ''}"><div class="list-card-head"><div><h3>${esc(item.name)}</h3><p>${esc(zone?.name || tr('common.noZone'))} · ${esc(days)}</p></div><span class="badge ${item.enabled ? 'active' : ''}">${esc(item.enabled ? tr('schedules.enabledState') : tr('schedules.disabledState'))}</span></div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('common.from'))}</small><strong>${esc(item.start_time)}</strong></div><div class="card-stat"><small>${esc(tr('common.to'))}</small><strong>${esc(item.end_time)}</strong></div><div class="card-stat"><small>${esc(tr('schedules.profile'))}</small><strong>${esc(item.preset === 'custom' ? fmtTemp(item.setpoint) : zonePresetLabel(item.preset))}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : tr('schedules.crossMidnight'))}</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-schedule" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-schedule" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : tr('schedules.crossMidnight'))}</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-schedule" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</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-schedule" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
}).join('') : `<div class="empty"><strong>${esc(tr('schedules.emptyTitle'))}</strong>${esc(tr('schedules.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ function renderAutomations() {
|
||||
const preset = item.action_zone_preset ? zonePresetLabel(item.action_zone_preset) : (item.action_preset ? zonePresetLabel(item.action_preset) : '—');
|
||||
return `<article class="list-card ${item.flow_id ? 'flow-generated-card' : ''}"><div class="list-card-head"><div><h3>${esc(item.name)}</h3><p>${esc(tr('automations.triggerSummary', { trigger: automationTriggerLabel(item), device: targetName }))}</p></div><span class="badge ${item.enabled ? 'active' : ''}">${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}</span></div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('common.power'))}</small><strong>${item.action.power == null ? '—' : item.action.power ? tr('common.on') : tr('common.off')}</strong></div><div class="card-stat"><small>${esc(tr('common.mode'))}</small><strong>${esc(mode)}</strong></div><div class="card-stat"><small>${esc(tr('groups.profile'))}</small><strong>${esc(preset)}</strong></div><div class="card-stat"><small>${esc(tr('automations.last'))}</small><strong>${item.last_fired_at ? new Date(item.last_fired_at).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' }) : '—'}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : (actionGroup ? `${tr('groups.group')}: ${targetName}` : `${tr('common.device')}: ${targetName}`))} · ${esc(tr('common.cooldown'))} ${item.cooldown_seconds}s</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-automation" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : (actionGroup ? `${tr('groups.group')}: ${targetName}` : `${tr('common.device')}: ${targetName}`))} · ${esc(tr('common.cooldown'))} ${item.cooldown_seconds}s</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-automation" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</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-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
}).join('') : `<div class="empty"><strong>${esc(tr('automations.emptyTitle'))}</strong>${esc(tr('automations.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
|
||||
+9
-9
@@ -98,9 +98,9 @@ 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>✎</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>${flow.enabled ? '✓' : '○'}</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="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="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'))}">•••</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>
|
||||
<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>`;
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ function renderFlowEditor() {
|
||||
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>
|
||||
<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'))}">×</button></div>
|
||||
<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>
|
||||
</article>`;
|
||||
@@ -534,7 +534,7 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`;
|
||||
else if (node.kind === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`;
|
||||
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">↕</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">×</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
}
|
||||
|
||||
function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join(''); return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-flow-config="value" value="${esc(c.value ?? '')}"></label></div>`; }
|
||||
@@ -864,9 +864,9 @@ function renderFlowPresetPreview(preset) {
|
||||
const req = flowPresetRequirements(preset);
|
||||
const favorite = flowPresetFavoriteIds().has(preset.id);
|
||||
const reqMarkup = req.requirements.length
|
||||
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? '✓' : '!'} ${esc(item.label)}</span>`).join('')
|
||||
: `<span class="flow-template-requirement ok">✓ ${esc(tr('flow.templateRequirementsReady'))}</span>`;
|
||||
host.innerHTML = `<div class="flow-template-preview-head"><div><span class="eyebrow">${esc(tr('flow.templatePreview'))}</span><h3>${esc(flowPresetText(preset.name) || preset.id)}</h3></div><button type="button" class="flow-template-favorite ${favorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${favorite ? '★' : '☆'}</button></div>
|
||||
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? uiIcon('check') : '!'} ${esc(item.label)}</span>`).join('')
|
||||
: `<span class="flow-template-requirement ok">${uiIcon('check')} ${esc(tr('flow.templateRequirementsReady'))}</span>`;
|
||||
host.innerHTML = `<div class="flow-template-preview-head"><div><span class="eyebrow">${esc(tr('flow.templatePreview'))}</span><h3>${esc(flowPresetText(preset.name) || preset.id)}</h3></div><button type="button" class="flow-template-favorite ${favorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${uiIcon(favorite ? 'star-filled' : 'star')}</button></div>
|
||||
<p>${esc(flowPresetText(preset.description))}</p>
|
||||
<div class="flow-template-preview-stats"><span>${esc(tr('flow.templateNodesCount', { count: preset.flow.nodes.length }))}</span><span>${esc(tr('flow.templateEdgesCount', { count: preset.flow.edges.length }))}</span></div>
|
||||
${flowPresetPreviewGraph(preset)}
|
||||
@@ -902,7 +902,7 @@ function renderFlowPresetBrowser(category = '') {
|
||||
const emptyKey = query ? 'flow.templateNoResults' : flowPresetActiveCategory === 'favorites' ? 'flow.templateNoFavorites' : flowPresetActiveCategory === 'recent' ? 'flow.templateNoRecent' : 'flow.templateNoResults';
|
||||
const cards = active.map(preset => {
|
||||
const isFavorite = favorites.has(preset.id), selected = preset.id === flowPresetPreviewId;
|
||||
return `<article class="flow-template-card ${selected ? 'selected' : ''}"><button type="button" class="flow-template-card-main" data-flow-template-preview="${esc(preset.id)}"><strong>${esc(flowPresetText(preset.name) || preset.id)}</strong><span>${esc(flowPresetText(preset.description))}</span></button><button type="button" class="flow-template-favorite ${isFavorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${isFavorite ? '★' : '☆'}</button></article>`;
|
||||
return `<article class="flow-template-card ${selected ? 'selected' : ''}"><button type="button" class="flow-template-card-main" data-flow-template-preview="${esc(preset.id)}"><strong>${esc(flowPresetText(preset.name) || preset.id)}</strong><span>${esc(flowPresetText(preset.description))}</span></button><button type="button" class="flow-template-favorite ${isFavorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${uiIcon(isFavorite ? 'star-filled' : 'star')}</button></article>`;
|
||||
}).join('');
|
||||
host.innerHTML = `<section class="flow-template-category-single"><div class="flow-template-category-head"><strong>${esc(flowPresetCategoryLabel(flowPresetActiveCategory))}</strong><span>${esc(flowPresetCategoryHint(flowPresetActiveCategory))}</span></div>${cards ? `<div class="flow-template-category-grid">${cards}</div>` : `<div class="empty compact"><span>${esc(tr(emptyKey))}</span></div>`}</section>`;
|
||||
if (!active.some(preset => preset.id === flowPresetPreviewId)) flowPresetPreviewId = active[0]?.id || '';
|
||||
@@ -983,7 +983,7 @@ function openFlowDryRun() {
|
||||
function renderFlowDryRunResult(result) {
|
||||
const host = $('#flowTestResults');
|
||||
const actionName = id => flowNodeById(id) ? `${flowNodeTitle(FLOW_NODE_META[flowNodeById(id).kind])}: ${flowNodeSummary(flowNodeById(id))}` : id;
|
||||
host.innerHTML = `<div class="flow-test-summary"><strong>${esc(result.summary || tr('flow.dryRun'))}</strong><span>${esc(tr('flow.compiledPreview', result.compiled || { schedules: 0, automations: 0 }))}</span></div>${(result.actions || []).map(action => `<article class="flow-test-action ${action.would_execute ? 'pass' : 'blocked'}"><div><strong>${esc(actionName(action.node_id))}</strong><span class="badge ${action.would_execute ? 'active' : ''}">${esc(action.would_execute ? tr('flow.wouldExecute') : action.matched ? tr('flow.blockedByOwnership') : tr('flow.conditionFalse'))}</span></div>${action.blocked_reason ? `<p>${esc(tr('flow.blockReason'))}: <code>${esc(action.blocked_reason)}</code></p>` : ''}<div class="flow-trace">${(action.trace || []).map(item => `<div><span>${item.matched ? '✓' : '×'} ${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title: item.kind }))}</span><code>${esc(JSON.stringify(item.actual))}</code></div>`).join('')}</div></article>`).join('')}`;
|
||||
host.innerHTML = `<div class="flow-test-summary"><strong>${esc(result.summary || tr('flow.dryRun'))}</strong><span>${esc(tr('flow.compiledPreview', result.compiled || { schedules: 0, automations: 0 }))}</span></div>${(result.actions || []).map(action => `<article class="flow-test-action ${action.would_execute ? 'pass' : 'blocked'}"><div><strong>${esc(actionName(action.node_id))}</strong><span class="badge ${action.would_execute ? 'active' : ''}">${esc(action.would_execute ? tr('flow.wouldExecute') : action.matched ? tr('flow.blockedByOwnership') : tr('flow.conditionFalse'))}</span></div>${action.blocked_reason ? `<p>${esc(tr('flow.blockReason'))}: <code>${esc(action.blocked_reason)}</code></p>` : ''}<div class="flow-trace">${(action.trace || []).map(item => `<div><span>${uiIcon(item.matched ? 'check' : 'close')} ${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title: item.kind }))}</span><code>${esc(JSON.stringify(item.actual))}</code></div>`).join('')}</div></article>`).join('')}`;
|
||||
}
|
||||
async function runFlowDryRun() {
|
||||
if (!app.flowDraft) return;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ function toast(message, error = false) {
|
||||
const host = $('#toastStack'); if (!host) return;
|
||||
const item = document.createElement('div');
|
||||
item.className = `toast-item ${error ? 'error' : 'success'}`;
|
||||
item.innerHTML = `<span class="toast-icon">${error ? '!' : '✓'}</span><div class="toast-copy"><strong>${esc(error ? tr('toast.errorTitle') : tr('toast.successTitle'))}</strong><span>${esc(message)}</span></div><button class="toast-close" type="button" aria-label="${esc(tr('actions.close'))}">×</button><i class="toast-progress"></i>`;
|
||||
item.innerHTML = `<span class="toast-icon">${error ? '!' : uiIcon('check')}</span><div class="toast-copy"><strong>${esc(error ? tr('toast.errorTitle') : tr('toast.successTitle'))}</strong><span>${esc(message)}</span></div><button class="toast-close" type="button" aria-label="${esc(tr('actions.close'))}">${uiIcon('close')}</button><i class="toast-progress"></i>`;
|
||||
host.appendChild(item);
|
||||
requestAnimationFrame(() => item.classList.add('show'));
|
||||
const remove = () => { item.classList.remove('show'); item.classList.add('leaving'); setTimeout(() => item.remove(), 220); };
|
||||
|
||||
+2
-2
@@ -280,9 +280,9 @@ function renderCustomBuilder() {
|
||||
const selected = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
|
||||
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'))}">×</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</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)}">×</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</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>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ if ('serviceWorker' in navigator && !APP_BASE.startsWith('/api/hassio_ingress/')
|
||||
|
||||
async function startApplication() {
|
||||
try {
|
||||
hydrateUiIcons();
|
||||
observeTopbarHeight();
|
||||
applyTheme();
|
||||
await loadLanguages();
|
||||
|
||||
+13
-1
@@ -22,6 +22,18 @@ function logDisplayMessage(item) {
|
||||
return `${message} · ${tr('history.haSensor')}: ${sensor}${zoneName ? ` · ${tr('common.zone')}: ${zoneName}` : ''}`;
|
||||
}
|
||||
|
||||
|
||||
function logCategoryIcon(category) {
|
||||
return {
|
||||
device: 'devices',
|
||||
zone: 'zones',
|
||||
automation: 'automation',
|
||||
integration: 'integration',
|
||||
settings: 'sliders',
|
||||
system: 'system-info',
|
||||
}[category] || 'system-info';
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
renderLogRetention();
|
||||
try {
|
||||
@@ -29,7 +41,7 @@ async function loadLogs() {
|
||||
const level = $('#logLevelFilter')?.value || 'all', category = $('#logCategoryFilter')?.value || 'all';
|
||||
const logs = (data.events || []).filter(item => (level === 'all' || item.level === level) && (category === 'all' || logCategory(item.kind) === category));
|
||||
$('#logList').innerHTML = logs.length
|
||||
? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}${logNotificationBadge(item)}</span><span class="message">${esc(logDisplayMessage(item))}</span></div>`).join('')
|
||||
? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${uiIcon(logCategoryIcon(logCategory(item.kind)))}${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}${logNotificationBadge(item)}</span><span class="message">${esc(logDisplayMessage(item))}</span></div>`).join('')
|
||||
: `<div class="empty"><strong>${esc(tr('logs.emptyTitle'))}</strong>${esc(tr('logs.emptyText'))}</div>`;
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
+2
-2
@@ -128,7 +128,7 @@ function rebuildCustomSelectMenu(select, state) {
|
||||
text.textContent = option.textContent || option.label || option.value || '—';
|
||||
const check = document.createElement('b');
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = option.selected ? '✓' : '';
|
||||
check.innerHTML = option.selected ? uiIcon('check') : '';
|
||||
button.append(text, check);
|
||||
|
||||
button.addEventListener('click', event => {
|
||||
@@ -310,7 +310,7 @@ function enhanceCustomSelect(select) {
|
||||
const chevron = document.createElement('span');
|
||||
chevron.className = 'custom-select-chevron';
|
||||
chevron.setAttribute('aria-hidden', 'true');
|
||||
chevron.textContent = '⌄';
|
||||
chevron.innerHTML = uiIcon('chevron-down');
|
||||
trigger.append(value, chevron);
|
||||
wrapper.appendChild(trigger);
|
||||
select.insertAdjacentElement('afterend', wrapper);
|
||||
|
||||
@@ -47,7 +47,7 @@ function renderSensorAliases() {
|
||||
const metricEntities = metricHaEntities(), flowEntities = flowHaEntities();
|
||||
host.innerHTML = entities.length ? entities.map(entity => {
|
||||
const badges = `${metricEntities.has(entity) ? `<span class="sensor-source-badge metrics">${esc(tr('settings.sensorMetricBadge'))}</span>` : ''}${flowEntities.has(entity) ? `<span class="sensor-source-badge flow">${esc(tr('settings.sensorFlowBadge'))}</span>` : ''}`;
|
||||
return `<div class="sensor-alias-row"><div class="sensor-alias-entity"><span class="mono" title="${esc(entity)}">${esc(entity)}</span>${badges ? `<span class="sensor-source-badges">${badges}</span>` : ''}</div><input data-sensor-alias="${esc(entity)}" value="${esc(app.sensorAliases?.[entity] || '')}" placeholder="${esc(tr('settings.aliasPlaceholder'))}"><button type="button" class="sensor-alias-clear" data-clear-sensor-alias="${esc(entity)}" title="${esc(tr('actions.clear'))}">×</button></div>`;
|
||||
return `<div class="sensor-alias-row"><div class="sensor-alias-entity"><span class="mono" title="${esc(entity)}">${esc(entity)}</span>${badges ? `<span class="sensor-source-badges">${badges}</span>` : ''}</div><input data-sensor-alias="${esc(entity)}" value="${esc(app.sensorAliases?.[entity] || '')}" placeholder="${esc(tr('settings.aliasPlaceholder'))}"><button type="button" class="sensor-alias-clear" data-clear-sensor-alias="${esc(entity)}" title="${esc(tr('actions.clear'))}">${uiIcon('close')}</button></div>`;
|
||||
}).join('') : `<div class="empty compact">${esc(tr('settings.noSensorAliases'))}</div>`;
|
||||
}
|
||||
|
||||
|
||||
+30
-42
@@ -450,6 +450,15 @@ h3 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.ui-icon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
flex: 0 0 auto;
|
||||
vertical-align: -0.125em;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.power-icon {
|
||||
display: block;
|
||||
width: 1em;
|
||||
@@ -3516,28 +3525,24 @@ button.outside-pill:focus-visible {
|
||||
opacity: .7
|
||||
}
|
||||
|
||||
.category-device .log-category:before {
|
||||
content: '▣ '
|
||||
.log-category {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .32rem;
|
||||
}
|
||||
|
||||
.category-zone .log-category:before {
|
||||
content: '◎ '
|
||||
.log-category .ui-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.category-automation .log-category:before {
|
||||
content: '⚙ '
|
||||
.flow-fit-button-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.category-integration .log-category:before {
|
||||
content: '↔ '
|
||||
}
|
||||
|
||||
.category-settings .log-category:before {
|
||||
content: '☷ '
|
||||
}
|
||||
|
||||
.category-system .log-category:before {
|
||||
content: '• '
|
||||
.flow-fit-button .ui-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
@media(max-width:720px) {
|
||||
@@ -8149,12 +8154,6 @@ button {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⊡";
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flow-palette {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -9239,12 +9238,6 @@ body.flow-editor-open {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⊡";
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#controlPlan .plan-card {
|
||||
@@ -9321,13 +9314,6 @@ body.flow-editor-open {
|
||||
font-size: 0 !important;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⤢";
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 700px) {
|
||||
@@ -9370,13 +9356,6 @@ body.flow-editor-open {
|
||||
font-size: 0 !important;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⤢";
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 700px) and (min-width: 500px) {
|
||||
@@ -9906,3 +9885,12 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px), (orientation: landscape) and (max-height: 700px) {
|
||||
.flow-fit-button-icon { display: inline-grid; place-items: center; }
|
||||
.flow-fit-button-label { display: none; }
|
||||
}
|
||||
@media (orientation: landscape) and (max-height: 700px) and (min-width: 500px) {
|
||||
.flow-fit-button-icon { display: none; }
|
||||
.flow-fit-button-label { display: inline; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user