v0.11.4
This commit is contained in:
Generated
+1
-1
@@ -633,7 +633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.11.3"
|
||||
version = "0.11.4"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.11.3"
|
||||
version = "0.11.4"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+20
-16
@@ -1,11 +1,11 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
2b03135cfb84c2df0f14380231e954882d743ba922c65ba3cbc56474057a039b ./Cargo.lock
|
||||
2bd686728663c50fa901cc3b35519da0e91e08e44994aff46783dedbd62409bd ./Cargo.toml
|
||||
ee9f0a05e85c3ffc081647716020ddfa26dfe128bc955eaed56a3b57cbbb81e6 ./Cargo.lock
|
||||
2cb81c4e01143414768f0592c4e362056ecf04678c465503789b6162fefa700c ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
fe279b20b6e9da85c1e639078c213875bfa842102017cda4d0d06120fe8fe3ba ./README.md
|
||||
a2713e3390c232ccf94e310ba18cb18ac47156ec050260f697fbc4257a9e1239 ./README.md
|
||||
41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs
|
||||
ec33deef383b0aee070d3fef68c54387d6dba4fc2fc9b56f4a902484c5338989 ./docs/API.md
|
||||
14188e547cd09ec63240cee480cf499ca479f300653fdf24dbef90a7175e4ec5 ./docs/API.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
@@ -17,7 +17,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||
629c10dd8b65cfde3a1db168cd6b5b642a87c4a4cd1149cc77121a121e525bbb ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
1168f40e21d3c21341353f254f0b79157133e9625f9cdcd552bf6bcfd444e7c9 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -26,9 +26,13 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assista
|
||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
69f9a7d4152036db8a48773d5c0fa364ab0fbb9cde1f28d07390023509d3544d ./lang/en.json
|
||||
36cbb4ab3233b21ef13ce1dd01f747c5067d406f246df8c52ea48b3a426d64f3 ./lang/pl.json
|
||||
7414e94d61d2dfd9734ee92a2a1423cae67e91e8733c213098b9bcb6a2885a22 ./lang/en.json
|
||||
1ff6dab25d8a29d971cd10de8d0df1b6173670ba1e6f65c0c669bf6b134bcf23 ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html
|
||||
007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js
|
||||
75999d965ea56a2cd961afe5771e6e9b1be513fab1dcafb480fbe9c0c232e7df ./mock/styles.css
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./mock/theme-init.js
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
c5276fecc6a33c5de912e5a73b0a829d4e18c18d57b9c0ee49179ef96f133fc7 ./presets/dual_threshold_control.json
|
||||
@@ -172,24 +176,24 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
b39ebf9538c078f2311d3064207da0fa46217037e1eb56566867028565719e53 ./web/index.html
|
||||
4f273e189008df110cd3cad1d323e5f07e2651f805419b427811d89fdd04245f ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
21b662914590e720dafefe4dc3dccc91dbeb963cce9ae91f007c835d2d0c1fe5 ./web/js/bootstrap.js
|
||||
72fa280996ba331a05bdb29a1f49b4e61bdd8964953c8ce1db952156620f08fe ./web/js/charts.js
|
||||
782ce96f3ec006c447e8c44c60ce4142e0fd830704a6ef0511a66505c12cb6bf ./web/js/core.js
|
||||
dee7c4292889ddd721c220e86fae909f0d662b585e6d9c5d9e2f8fa85d9ed2ad ./web/js/charts.js
|
||||
87064e7d504dc2afbc1aa3a1926fcec0cc80b31e2a6a6d931d7af7fc4f519b20 ./web/js/core.js
|
||||
dfc123885a9b6b5c1d07c12d2d2625ff15aa0844e5cd3736092ccd0a360da39c ./web/js/dashboard.js
|
||||
f53e2da5c48b55cf0191ffeb970a2c053c66e62d29099e3c5b5fccc8d89440cc ./web/js/entities.js
|
||||
9a0e87911f78cd8a969f4c57ce74774f5fa829fdecd79494d9a7f9d5804f9792 ./web/js/events.js
|
||||
2bcf1292d26815e4919492c558601d8f0ddd61163f7ec916b562a40b409abdde ./web/js/flows.js
|
||||
3f5b9a4d62c994edded0ce24fb9ea45141d935a1928d4f68fb67f8ca287bafe1 ./web/js/entities.js
|
||||
cdd11ab0fac33b7b1749b8db4e30486e9d0258b65fe54fd56c3a520b2b72b2d7 ./web/js/events.js
|
||||
d4aea1d6ec595000eaddca3b6f0e3d4003901365ece7788147a6ed207cffcc78 ./web/js/flows.js
|
||||
f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js
|
||||
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
|
||||
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
|
||||
7f23507b2f1f632d69b466688f68f3f0bf661dc6856a1ed14a462e063114a980 ./web/js/navigation.js
|
||||
3dcfc24f238ad1310155707268b60c5a415f2a8977ceba48c7e695b7135d5705 ./web/js/navigation.js
|
||||
c56059693d09e35261dc5dd1940289ffc67c82ea7c547750d57e6c79fe75b05b ./web/js/realtime.js
|
||||
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
|
||||
34513d9ec2e45c60443dffe028eea74f415b8a29016df6af3377a80a4ac0df5b ./web/js/settings-ui.js
|
||||
bdeb55546a1dff5e5bdfde4b114d6330f025df1b62e528bf18a93ad6dd4c3e83 ./web/js/settings-ui.js
|
||||
aa6a2ba22648de44547efd09f41c4335ebe38ed564f4684e942d7827df40f1a2 ./web/js/settings.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
3069a9860857d4c9f1d820c6066cc9c9326d495b49083a422d327249032dd9e4 ./web/styles.css
|
||||
75999d965ea56a2cd961afe5771e6e9b1be513fab1dcafb480fbe9c0c232e7df ./web/styles.css
|
||||
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Current release: 0.11.3**
|
||||
**Current release: 0.11.4**
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.11.3**.
|
||||
HTTP and WebSocket API for GREE Controller **0.11.4**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -213,7 +213,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.4",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -237,7 +237,7 @@ Returns the initial Web UI snapshot:
|
||||
"settings": {},
|
||||
"outdoor_temperature": null,
|
||||
"system": {
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.4",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.11.1",
|
||||
"version": "0.11.4",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
+31
-3
@@ -474,7 +474,7 @@
|
||||
"settings.eventRetentionDays": "Event/log retention (days)",
|
||||
"settings.debugHint": "Live diagnostics displayed on every application page.",
|
||||
"settings.sensorAliases": "Sensor aliases",
|
||||
"settings.sensorAliasesHint": "Friendly names are used only in charts and selectors. Home Assistant requests always use the original entity_id.",
|
||||
"settings.sensorAliasesHint": "Friendly names are used in charts and selectors. Badges show whether an entity is collected as metrics, used by Flow, or both. A shared Flow input by itself does not enable metric collection.",
|
||||
"settings.aliasPlaceholder": "Friendly name",
|
||||
"settings.noSensorAliases": "No Home Assistant sensors are known yet. Add an entity_id below.",
|
||||
"settings.aliasRequired": "Enter both entity_id and alias.",
|
||||
@@ -730,6 +730,8 @@
|
||||
"history.zoomOut": "Zoom out",
|
||||
"history.zoomReset": "Reset zoom",
|
||||
"history.zoomIn": "Zoom in",
|
||||
"history.fullscreen": "Enlarge chart",
|
||||
"history.exitFullscreen": "Close enlarged chart",
|
||||
"house.powerOffConfirm": "Turn all air conditioners off? All thermostats will remain locally OFF with no timer. You can later enable an individual thermostat, a group, or the whole house.",
|
||||
"zones.temporaryStarting": "Starting…",
|
||||
"notifications.applicationTokenPlaceholder": "Application token",
|
||||
@@ -943,7 +945,7 @@
|
||||
"flow.sharedInputType": "Source type",
|
||||
"flow.sharedInputAdd": "Add input",
|
||||
"flow.sharedInputsTitle": "Shared Flow inputs",
|
||||
"flow.sharedInputsHint": "Define reusable value sources once, then use them in many Flows. Operators and comparison values are configured only in Flow blocks.",
|
||||
"flow.sharedInputsHint": "Define reusable value sources once, then use them in many Flows. Operators and comparison values are configured only in Flow blocks. HA sensors added only here are not stored as metrics.",
|
||||
"flow.sharedInputsEmpty": "No shared inputs",
|
||||
"flow.sharedInputsEmptyHint": "Add a constant, Home Assistant state, or another shared source for multiple Flows.",
|
||||
"flow.sharedInputNodeHint": "This block uses a central value source. The comparison below belongs only to this Flow.",
|
||||
@@ -1056,6 +1058,12 @@
|
||||
"flow.durationSeconds": "Duration [s]",
|
||||
"flow.stableForHint": "The input condition must remain true continuously for the configured duration. State survives restarts.",
|
||||
"flow.serviceData": "Service data (JSON)",
|
||||
"flow.serviceExample": "Example: to turn on switch.gas use domain = switch, service = turn_on and entity_id = switch.gas.",
|
||||
"flow.enabledState": "Enabled",
|
||||
"flow.disabledState": "Disabled",
|
||||
"flow.enabledSelect": "Flow state",
|
||||
"settings.sensorMetricBadge": "Metrics",
|
||||
"settings.sensorFlowBadge": "Flow",
|
||||
"flow.invalidJson": "Invalid JSON",
|
||||
"flow.mean": "Mean",
|
||||
"flow.median": "Median",
|
||||
@@ -1101,6 +1109,26 @@
|
||||
"flow.draftDisabledHint": "A draft is always disabled. Finish the Flow and save it again to create executable tasks.",
|
||||
"flow.draftCannotEnable": "Finish and save the Flow first. A draft cannot be enabled.",
|
||||
"flow.saveAsDraftConfirm": "The Flow cannot be saved as executable:\n\n{reason}\n\nSave it as a draft instead? The draft will be disabled and will not create any tasks or schedules.",
|
||||
"flow.savedAsDraft": "Flow saved as a disabled draft. You can return to it later."
|
||||
"flow.savedAsDraft": "Flow saved as a disabled draft. You can return to it later.",
|
||||
"automationHub.navigation": "Automation section",
|
||||
"nav.sectionControl": "Control",
|
||||
"nav.sectionAutomation": "Automation",
|
||||
"nav.sectionData": "Data & system",
|
||||
"nav.homeAssistantShort": "Home Assistant",
|
||||
"actions.more": "More options",
|
||||
"flow.addBlock": "Add block",
|
||||
"flow.searchBlocks": "Search blocks…",
|
||||
"flow.noBlocksFound": "No matching blocks",
|
||||
"flow.noBlocksFoundHint": "Change the search phrase or clear the filter.",
|
||||
"flow.zoomControls": "Flow canvas zoom",
|
||||
"flow.zoomOut": "Zoom out",
|
||||
"flow.zoomIn": "Zoom in",
|
||||
"flow.fitView": "Fit",
|
||||
"flow.unsavedChanges": "Unsaved changes",
|
||||
"flow.savedState": "Saved",
|
||||
"flow.expandSettings": "Expand block settings",
|
||||
"flow.notSavedYet": "Not saved yet",
|
||||
"schedules.enabledState": "Enabled",
|
||||
"schedules.disabledState": "Disabled"
|
||||
}
|
||||
}
|
||||
|
||||
+33
-5
@@ -474,7 +474,7 @@
|
||||
"settings.eventRetentionDays": "Retencja zdarzeń/logów (dni)",
|
||||
"settings.debugHint": "Diagnostyka na żywo wyświetlana na każdej podstronie aplikacji.",
|
||||
"settings.sensorAliases": "Aliasy czujników",
|
||||
"settings.sensorAliasesHint": "Przyjazne nazwy są używane tylko na wykresach i w selektorach. Zapytania do Home Assistanta zawsze używają oryginalnego entity_id.",
|
||||
"settings.sensorAliasesHint": "Przyjazne nazwy są używane na wykresach i w selektorach. Oznaczenia pokazują, czy encja trafia do metryk, jest używana we Flow, czy w obu miejscach. Samo wspólne wejście Flow nie uruchamia zapisu metryk.",
|
||||
"settings.aliasPlaceholder": "Przyjazna nazwa",
|
||||
"settings.noSensorAliases": "Nie wykryto jeszcze czujników Home Assistant. Możesz dodać entity_id poniżej.",
|
||||
"settings.aliasRequired": "Podaj entity_id oraz alias.",
|
||||
@@ -730,6 +730,8 @@
|
||||
"history.zoomOut": "Pomniejsz wykres",
|
||||
"history.zoomReset": "Przywróć powiększenie",
|
||||
"history.zoomIn": "Powiększ wykres",
|
||||
"history.fullscreen": "Powiększ wykres",
|
||||
"history.exitFullscreen": "Zamknij powiększony wykres",
|
||||
"house.powerOffConfirm": "Wyłączyć wszystkie klimatyzatory? Wszystkie termostaty pozostaną lokalnie WYŁ. bez timera. Później możesz włączyć pojedynczy termostat, grupę albo cały dom.",
|
||||
"zones.temporaryStarting": "Uruchamianie…",
|
||||
"notifications.applicationTokenPlaceholder": "Token aplikacji",
|
||||
@@ -814,7 +816,7 @@
|
||||
"zones.queueCancelAllConfirm": "Anulować wszystkie oczekujące zadania ({count}) ochrony sprężarki?",
|
||||
"nav.flows": "Flow",
|
||||
"flow.eyebrow": "Logika wizualna",
|
||||
"flow.new": "Nowy Flow",
|
||||
"flow.new": "Nowe flow",
|
||||
"flow.infoTitle": "Jedno miejsce dla harmonogramów i automatyzacji",
|
||||
"flow.infoText": "Układasz logikę z bloków. GREE Controller tłumaczy Flow na generowane harmonogramy i automatyzacje. Wygenerowane wpisy są tylko do odczytu poza tym edytorem.",
|
||||
"flow.sourceOfTruth": "Flow = źródło logiki",
|
||||
@@ -932,7 +934,7 @@
|
||||
"flow.listCount": "{count} Flow",
|
||||
"flow.quickToggleHint": "Szybko włącz lub wyłącz Flow bez otwierania edytora",
|
||||
"flow.quickEnabled": "Flow włączony",
|
||||
"flow.quickDisabled": "Flow wyłączony",
|
||||
"flow.quickDisabled": "Flow wyłączone",
|
||||
"flow.selectAll": "Zaznacz wszystko",
|
||||
"flow.clearSelection": "Wyczyść zaznaczenie",
|
||||
"flow.selectedCount": "Zaznaczono: {count}",
|
||||
@@ -943,7 +945,7 @@
|
||||
"flow.sharedInputType": "Typ źródła",
|
||||
"flow.sharedInputAdd": "Dodaj wejście",
|
||||
"flow.sharedInputsTitle": "Wspólne wejścia Flow",
|
||||
"flow.sharedInputsHint": "Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Operatory i wartości porównań ustawia się wyłącznie w blokach Flow.",
|
||||
"flow.sharedInputsHint": "Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Operatory i wartości porównań ustawia się wyłącznie w blokach Flow. Czujniki HA dodane tylko tutaj nie są zapisywane do metryk.",
|
||||
"flow.sharedInputsEmpty": "Brak wspólnych wejść",
|
||||
"flow.sharedInputsEmptyHint": "Dodaj stałą, stan Home Assistant albo inne współdzielone źródło dla wielu Flow.",
|
||||
"flow.sharedInputNodeHint": "Ten blok korzysta z centralnego źródła wartości. Poniższe porównanie należy tylko do tego Flow.",
|
||||
@@ -1056,6 +1058,12 @@
|
||||
"flow.durationSeconds": "Czas [s]",
|
||||
"flow.stableForHint": "Warunek na wejściu musi pozostać prawdziwy bez przerwy przez podany czas. Stan jest zachowywany po restarcie.",
|
||||
"flow.serviceData": "Dane usługi (JSON)",
|
||||
"flow.serviceExample": "Przykład: aby włączyć switch.gas ustaw domain = switch, service = turn_on i entity_id = switch.gas.",
|
||||
"flow.enabledState": "Włączone",
|
||||
"flow.disabledState": "Wyłączone",
|
||||
"flow.enabledSelect": "Stan Flow",
|
||||
"settings.sensorMetricBadge": "Metryki",
|
||||
"settings.sensorFlowBadge": "Flow",
|
||||
"flow.invalidJson": "Nieprawidłowy JSON",
|
||||
"flow.mean": "Średnia",
|
||||
"flow.median": "Mediana",
|
||||
@@ -1101,6 +1109,26 @@
|
||||
"flow.draftDisabledHint": "Szkic jest zawsze wyłączony. Dokończ Flow i zapisz go ponownie, aby utworzyć zadania wykonawcze.",
|
||||
"flow.draftCannotEnable": "Najpierw dokończ i zapisz Flow. Szkicu nie można włączyć.",
|
||||
"flow.saveAsDraftConfirm": "Flow nie może zostać zapisany jako wykonywalny:\n\n{reason}\n\nZapisać go jako szkic? Szkic będzie wyłączony i nie utworzy żadnych zadań ani harmonogramów.",
|
||||
"flow.savedAsDraft": "Flow zapisany jako wyłączony szkic. Możesz wrócić do niego później."
|
||||
"flow.savedAsDraft": "Flow zapisany jako wyłączony szkic. Możesz wrócić do niego później.",
|
||||
"automationHub.navigation": "Sekcja automatyzacji",
|
||||
"nav.sectionControl": "Sterowanie",
|
||||
"nav.sectionAutomation": "Automatyzacja",
|
||||
"nav.sectionData": "Dane i system",
|
||||
"nav.homeAssistantShort": "Home Assistant",
|
||||
"actions.more": "Więcej opcji",
|
||||
"flow.addBlock": "Dodaj blok",
|
||||
"flow.searchBlocks": "Szukaj bloków…",
|
||||
"flow.noBlocksFound": "Brak pasujących bloków",
|
||||
"flow.noBlocksFoundHint": "Zmień wyszukiwaną frazę lub wyczyść filtr.",
|
||||
"flow.zoomControls": "Powiększenie obszaru Flow",
|
||||
"flow.zoomOut": "Pomniejsz",
|
||||
"flow.zoomIn": "Powiększ",
|
||||
"flow.fitView": "Dopasuj",
|
||||
"flow.unsavedChanges": "Niezapisane zmiany",
|
||||
"flow.savedState": "Zapisano",
|
||||
"flow.expandSettings": "Rozwiń ustawienia bloku",
|
||||
"flow.notSavedYet": "Jeszcze nie zapisano",
|
||||
"schedules.enabledState": "Włączony",
|
||||
"schedules.disabledState": "Wyłączony"
|
||||
}
|
||||
}
|
||||
|
||||
+1302
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
document.addEventListener('DOMContentLoaded',()=>{
|
||||
const q=new URLSearchParams(location.search);
|
||||
document.documentElement.dataset.theme='dark';
|
||||
const view=q.get('view')||'dashboard';
|
||||
document.querySelectorAll('.view').forEach(v=>v.classList.toggle('active',v.dataset.view===view));
|
||||
if(view==='dashboard'){
|
||||
document.querySelector('#summaryText').textContent='3 urządzenia online · 2 aktywne termostaty';
|
||||
document.querySelector('#metrics').innerHTML='<span>Online <b>3/3</b></span><span>Aktywne <b>2</b></span>';
|
||||
document.querySelector('#heroTemperature').innerHTML='21.4<small>°C</small>';
|
||||
document.querySelector('#houseClimate').innerHTML='<div class="house-climate-head"><div><h3>Tryb domu</h3><p>Wspólne ustawienia dla stref.</p></div><span class="outside-pill">Na zewnątrz 12.8°C</span></div><div class="house-mode-row"><button class="active">Grzanie</button><button>Chłodzenie</button></div><div class="house-preset-row"><button>Auto</button><button class="active">Komfort</button><button>Sen</button><button>Poza domem</button></div>';
|
||||
document.querySelector('#controlPlan').innerHTML='<article class="list-card"><div class="list-card-head"><div><h3>Salon</h3><p>Utrzymuj 21.5°C do 22:30</p></div><span class="badge">Aktywne</span></div></article><article class="list-card"><div class="list-card-head"><div><h3>Sypialnia</h3><p>Tryb nocny od 22:30</p></div><span class="badge">za 2h</span></div></article>';
|
||||
}
|
||||
if(view==='flows'){
|
||||
document.querySelector('#flowListCount').textContent='3 flow';
|
||||
document.querySelector('#flowList').innerHTML=`<article class="list-card flow-card"><div class="list-card-head"><div><h3>Okno w salonie</h3><p>Wyłącza grzanie, gdy okno jest otwarte.</p></div><button class="zone-enable-toggle active"><span>✓</span>Włączone</button></div><div class="card-stats"><div class="card-stat"><small>Bloki</small><strong>5</strong></div><div class="card-stat"><small>Harmonogramy</small><strong>0</strong></div><div class="card-stat"><small>Automatyzacje</small><strong>1</strong></div></div><div class="card-footer"><small>1 automatyzacja</small><div class="card-menu"><button>Otwórz edytor</button><button class="danger">Usuń</button></div></div></article><article class="list-card flow-card is-disabled"><div class="list-card-head"><div><h3>Tani prąd</h3><p>Podbija temperaturę przy niskiej cenie energii.</p></div><button class="zone-enable-toggle"><span>○</span>Wyłączone</button></div><div class="card-stats"><div class="card-stat"><small>Bloki</small><strong>7</strong></div><div class="card-stat"><small>Harmonogramy</small><strong>1</strong></div><div class="card-stat"><small>Automatyzacje</small><strong>2</strong></div></div><div class="card-footer"><small>1 harmonogram · 2 automatyzacje</small><div class="card-menu"><button>Otwórz edytor</button><button class="danger">Usuń</button></div></div></article>`;
|
||||
}
|
||||
if(q.get('editor')==='1'){
|
||||
document.querySelector('#flowEditor').hidden=false; document.body.classList.add('flow-editor-open');
|
||||
document.querySelector('#flowName').hidden=true; document.querySelector('#flowNameView').hidden=false; document.querySelector('#flowNameText').textContent='Okno w salonie';
|
||||
document.querySelector('#flowCompileStatus').textContent='0 harm. · 1 autom.'; document.querySelector('#flowSaveStatus').textContent='Zapisano';
|
||||
document.querySelector('#flowInterpretation').textContent='Jeśli okno w salonie jest otwarte przez 30 s, wyłącz termostat Salon.';
|
||||
document.querySelector('#flowRuntimeSummary').textContent='Ostatnie wykonanie: 07:12 · wynik: prawda';
|
||||
document.querySelector('#flowEmptyHint').style.display='none';
|
||||
document.querySelector('#flowNodes').innerHTML=`<div class="flow-node flow-node-sensor selected" style="left:60px;top:80px"><div class="flow-node-head">Wspólne wejście <button>×</button></div><div class="flow-node-body">Okno w salonie<br><div class="flow-node-current"><span>Teraz</span><strong>otwarte</strong></div></div><button class="flow-port flow-port-out"></button></div><div class="flow-node flow-node-timeop" style="left:300px;top:160px"><button class="flow-port flow-port-in"></button><div class="flow-node-head">Utrzymuje się przez… <button>×</button></div><div class="flow-node-body">30 sekund</div><button class="flow-port flow-port-out"></button></div><div class="flow-node flow-node-action" style="left:550px;top:95px"><button class="flow-port flow-port-in"></button><div class="flow-node-head">Termostat <button>×</button></div><div class="flow-node-body">Salon · wyłącz</div></div>`;
|
||||
document.querySelector('#flowInspector').classList.add('has-selection');
|
||||
document.querySelector('#flowInspector').innerHTML='<div class="flow-inspector-head"><div><span class="eyebrow">Ustawienia bloku</span><h3>Wspólne wejście</h3></div><div class="flow-inspector-head-actions"><button class="flow-inspector-close">×</button></div></div><label><span>Źródło</span><select><option>Okno w salonie</option></select></label><p class="field-note">Czujnik używany tylko we Flow. Nie jest zapisywany do metryk.</p>';
|
||||
}
|
||||
});
|
||||
+6763
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
(() => {
|
||||
const row = document.cookie.split('; ').find(item => item.startsWith('gree_controller_theme='));
|
||||
const requested = row ? decodeURIComponent(row.split('=').slice(1).join('=')) : 'system';
|
||||
const resolved = requested === 'light' || requested === 'dark'
|
||||
? requested
|
||||
: (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
})();
|
||||
+64
-13
@@ -182,6 +182,11 @@
|
||||
<h1 data-i18n="nav.flows">Flow</h1>
|
||||
</div><div class="section-actions flow-page-actions"><button class="secondary" type="button" data-action="import-flow" data-i18n="flow.import">Import Flow</button><button class="primary" type="button" data-action="new-flow" data-i18n="flow.new">New Flow</button></div>
|
||||
</div>
|
||||
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<button type="button" class="active" data-go="flows" data-i18n="nav.flows">Flow</button>
|
||||
<button type="button" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
|
||||
<button type="button" data-go="automations" data-i18n="nav.automations">Automations</button>
|
||||
</nav>
|
||||
<div class="panel flow-info-panel">
|
||||
<div><strong data-i18n="flow.infoTitle">One place for schedules and automations</strong>
|
||||
<p data-i18n="flow.infoText">Build the logic from blocks. GREE Controller translates Flow into generated schedules and automations. Generated entries are read-only outside this editor.</p>
|
||||
@@ -200,6 +205,11 @@
|
||||
<h1 data-i18n="nav.schedules">Schedules</h1>
|
||||
</div><button class="secondary" data-open="scheduleDialog" data-i18n="actions.add">Add</button>
|
||||
</div>
|
||||
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<button type="button" data-go="flows" data-i18n="nav.flows">Flow</button>
|
||||
<button type="button" class="active" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
|
||||
<button type="button" data-go="automations" data-i18n="nav.automations">Automations</button>
|
||||
</nav>
|
||||
<div class="panel schedule-template-panel">
|
||||
<div>
|
||||
<h3 data-i18n="schedules.quickSetup">Quick schedule setup</h3>
|
||||
@@ -228,6 +238,11 @@
|
||||
<h1 data-i18n="nav.automations">Automations</h1>
|
||||
</div><button class="secondary" data-open="automationDialog" data-i18n="actions.add">Add</button>
|
||||
</div>
|
||||
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<button type="button" data-go="flows" data-i18n="nav.flows">Flow</button>
|
||||
<button type="button" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
|
||||
<button type="button" class="active" data-go="automations" data-i18n="nav.automations">Automations</button>
|
||||
</nav>
|
||||
<div class="panel automation-hint"><strong data-i18n="automations.smartHintTitle">Use thermostat presets for daily
|
||||
comfort</strong>
|
||||
<p data-i18n="automations.smartHint">Schedules and zone presets handle normal heating/cooling automatically.
|
||||
@@ -396,12 +411,12 @@
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block flow-shared-settings" id="flowSharedInputsSettings">
|
||||
<div class="settings-block-head flow-shared-settings-head"><div><h3 data-i18n="flow.sharedInputsTitle">Wspólne wejścia Flow</h3><p data-i18n="flow.sharedInputsHint">Zdefiniuj wartości, stany i czujniki raz, a potem używaj ich w wielu Flow. Operator jest opcjonalny.</p></div><button type="button" class="primary flow-shared-add-button" id="addFlowSharedInput" data-i18n="flow.sharedInputAdd">Dodaj wejście</button></div>
|
||||
<div class="settings-block-head flow-shared-settings-head"><div><h3 data-i18n="flow.sharedInputsTitle">Wspólne wejścia Flow</h3><p data-i18n="flow.sharedInputsHint">Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Czujniki HA dodane tylko tutaj nie są zapisywane do metryk.</p></div><button type="button" class="primary flow-shared-add-button" id="addFlowSharedInput" data-i18n="flow.sharedInputAdd">Dodaj wejście</button></div>
|
||||
<input type="hidden" id="flowSharedInputsRevision" name="flow_inputs_revision" value="[]"><div id="flowSharedInputList" class="flow-shared-input-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.sensorAliases">Sensor aliases</h3><p data-i18n="settings.sensorAliasesHint">Friendly names are used in charts and selectors; the original entity_id remains unchanged.</p></div></div>
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.sensorAliases">Sensor aliases</h3><p data-i18n="settings.sensorAliasesHint">Badges show whether an entity is collected as metrics, used by Flow, or both.</p></div></div>
|
||||
<div class="sensor-alias-manager">
|
||||
<div id="sensorAliasList" class="sensor-alias-list"></div>
|
||||
<div class="sensor-alias-add"><input id="sensorAliasEntity" placeholder="sensor.gabinet_temperature"><input id="sensorAliasName" data-i18n-placeholder="settings.aliasPlaceholder" placeholder="Gabinet"><button type="button" class="secondary" id="addSensorAlias" data-i18n="actions.add">Add</button></div>
|
||||
@@ -694,7 +709,8 @@
|
||||
<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><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" data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint" placeholder="My automation"></div></div></div>
|
||||
<div class="flow-editor-actions"><button class="secondary" type="button" data-action="flow-templates" data-i18n="flow.templates">Templates</button><button class="secondary" type="button" data-action="import-flow" data-i18n="flow.import">Import</button><button class="secondary" type="button" data-action="export-flow" data-i18n="flow.export">Export</button><button class="secondary" type="button" data-action="flow-dry-run" data-i18n="flow.dryRun">Dry-run</button><button class="secondary" type="button" data-action="flow-logs" data-i18n="flow.logs">Logs</button><label class="flow-enabled"><input type="checkbox" id="flowEnabled" checked><span data-i18n="common.enabled">Enabled</span></label><span id="flowCompileStatus" class="badge"></span><button class="primary" type="button" data-action="save-flow" data-i18n="actions.save">Save</button></div>
|
||||
<div class="flow-editor-actions"><button class="secondary flow-desktop-action" type="button" data-action="flow-templates" data-i18n="flow.templates">Templates</button><button class="secondary flow-desktop-action" type="button" data-action="import-flow" data-i18n="flow.import">Import</button><button class="secondary flow-desktop-action" type="button" data-action="export-flow" data-i18n="flow.export">Export</button><button class="secondary flow-desktop-action" type="button" data-action="flow-dry-run" data-i18n="flow.dryRun">Dry-run</button><button class="secondary flow-desktop-action" type="button" data-action="flow-logs" data-i18n="flow.logs">Logs</button><label class="flow-enabled"><input type="checkbox" id="flowEnabled" checked><span id="flowEnabledLabel" data-i18n="common.enabled">Enabled</span></label><span id="flowSaveStatus" class="flow-save-status" aria-live="polite"></span><span id="flowCompileStatus" class="badge flow-compile-status"></span><button class="secondary flow-mobile-actions-button" type="button" data-action="flow-mobile-actions" data-i18n="flow.actions">Actions</button></div>
|
||||
<button class="primary flow-save-button" type="button" data-action="save-flow" data-i18n="actions.save">Save</button>
|
||||
</header>
|
||||
<div class="flow-editor-body">
|
||||
<aside class="flow-palette">
|
||||
@@ -714,20 +730,40 @@
|
||||
<button type="button" data-flow-add="ha_service_action" data-i18n="flow.node.haServiceAction">Home Assistant: usługa</button></div>
|
||||
</aside>
|
||||
<div class="flow-workspace-wrap">
|
||||
<div class="flow-workspace-toolbar"><button type="button" class="secondary" data-action="flow-select-all" data-i18n="flow.selectAll">Zaznacz wszystko</button><button type="button" class="secondary" data-action="flow-clear-selection" data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><span id="flowSelectionCount" class="badge"></span></div>
|
||||
<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 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" data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><span id="flowSelectionCount" 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" 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">Fit</button></div></div>
|
||||
<div id="flowWorkspace" class="flow-workspace" tabindex="0">
|
||||
<svg id="flowEdges" class="flow-edges" aria-hidden="true"></svg><div id="flowNodes" class="flow-nodes"></div>
|
||||
<div id="flowEmptyHint" class="flow-empty-hint"><strong data-i18n="flow.emptyCanvas">Start by adding blocks</strong><span data-i18n="flow.emptyCanvasHint">Conditions go on the left, actions on the right.</span></div>
|
||||
<div id="flowCanvas" class="flow-canvas"><svg id="flowEdges" class="flow-edges" aria-hidden="true"></svg><div id="flowNodes" class="flow-nodes"></div></div>
|
||||
<div id="flowEmptyHint" class="flow-empty-hint"><strong data-i18n="flow.emptyCanvas">Start by adding blocks</strong><span data-i18n="flow.emptyCanvasHint">Conditions go on the left, actions on the right.</span><button type="button" class="primary" data-action="flow-add-block" data-i18n="flow.addBlock">Add block</button></div>
|
||||
</div>
|
||||
<div class="flow-natural-preview">
|
||||
<div class="flow-preview-item flow-preview-interpretation"><strong data-i18n="flow.interpretation">Interpretation</strong><span id="flowInterpretation"></span></div>
|
||||
<div class="flow-preview-item flow-preview-runtime" id="flowRuntimeInfo"><strong data-i18n="flow.runtimeInfo">Flow cycle</strong><span id="flowRuntimeSummary"></span></div>
|
||||
<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.interpretation">Interpretation</strong><span class="flow-preview-toggle-icon" 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>
|
||||
<div class="flow-preview-item flow-preview-runtime" id="flowRuntimeInfo"><strong data-i18n="flow.runtimeInfo">Flow cycle</strong><span id="flowRuntimeSummary"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<aside id="flowInspector" class="flow-inspector"><div class="empty compact"><strong data-i18n="flow.selectBlock">Select a block</strong><span data-i18n="flow.selectBlockHint">Its settings will appear here.</span></div></aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<dialog id="flowBlockDialog" class="sheet flow-block-dialog">
|
||||
<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>
|
||||
<div class="flow-block-search"><input id="flowBlockSearch" type="search" autocomplete="off" data-i18n-placeholder="flow.searchBlocks" placeholder="Search blocks…"></div>
|
||||
<div id="flowBlockLibrary" class="flow-block-library"></div>
|
||||
</dialog>
|
||||
|
||||
<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></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>
|
||||
<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>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<input id="flowImportFile" type="file" accept="application/json,.json,.flow.json" hidden>
|
||||
|
||||
<dialog id="flowSharedInputDialog">
|
||||
@@ -775,22 +811,37 @@
|
||||
</dialog>
|
||||
|
||||
<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="devices"><span>▣</span><b data-i18n="nav.devices">Devices</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>
|
||||
<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 data-i18n="nav.schedules">Schedules</b></button>
|
||||
<button class="desktop-nav-only" data-nav="automations"><span>⌁</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 data-nav="more"><span>•••</span><b data-i18n="nav.more">More</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span>▣</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 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>
|
||||
</nav>
|
||||
|
||||
<dialog id="moreDialog" class="sheet">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="nav.more">More</h2><button data-close>×</button>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<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="flows"><span data-i18n="nav.flows">Flow</span><span>›</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>
|
||||
<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="homeassistant"><span data-i18n="nav.homeAssistant">Home Assistant /
|
||||
|
||||
+64
-8
@@ -134,22 +134,71 @@ function timeLabel(ts, hours) {
|
||||
return new Date(ts).toLocaleString(locale(), options);
|
||||
}
|
||||
|
||||
function chartCardIsFullscreen(card) {
|
||||
return !!card && card.classList.contains('chart-fullscreen-fallback');
|
||||
}
|
||||
|
||||
function updateChartFullscreenButton(card) {
|
||||
if (!card) return;
|
||||
const button = card.querySelector('[data-chart-fullscreen]');
|
||||
if (!button) return;
|
||||
const active = chartCardIsFullscreen(card);
|
||||
button.textContent = active ? '×' : '⛶';
|
||||
button.title = tr(active ? 'history.exitFullscreen' : 'history.fullscreen');
|
||||
button.setAttribute('aria-label', button.title);
|
||||
}
|
||||
|
||||
function closeChartPreview(card) {
|
||||
if (!card) return;
|
||||
card.classList.remove('chart-fullscreen-fallback');
|
||||
document.body.classList.remove('chart-fullscreen-open');
|
||||
updateChartFullscreenButton(card);
|
||||
const canvas = card.querySelector('canvas[id]');
|
||||
if (canvas?.id) requestAnimationFrame(() => redrawHistoryChart(canvas.id));
|
||||
}
|
||||
|
||||
function toggleChartFullscreen(id) {
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
if (!card) return;
|
||||
|
||||
const active = chartCardIsFullscreen(card);
|
||||
const opened = document.querySelector('.history-chart-card.chart-fullscreen-fallback');
|
||||
if (opened && opened !== card) closeChartPreview(opened);
|
||||
|
||||
if (active) {
|
||||
closeChartPreview(card);
|
||||
return;
|
||||
}
|
||||
|
||||
card.classList.add('chart-fullscreen-fallback');
|
||||
document.body.classList.add('chart-fullscreen-open');
|
||||
updateChartFullscreenButton(card);
|
||||
requestAnimationFrame(() => redrawHistoryChart(id));
|
||||
}
|
||||
|
||||
function prepareCanvas(canvas, height) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrapWidth = Math.floor(canvas.parentElement?.clientWidth || rect.width || 720);
|
||||
const wrap = canvas.parentElement;
|
||||
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
|
||||
const card = canvas.closest('.history-chart-card');
|
||||
const fullscreenHeight = chartCardIsFullscreen(card) ? Math.floor(wrap?.clientHeight || 0) : 0;
|
||||
const actualHeight = Math.max(height, fullscreenHeight || 0);
|
||||
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, 4);
|
||||
const width = Math.max(720, Math.floor(Math.max(720, wrapWidth) * zoom));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr; canvas.height = height * dpr;
|
||||
canvas.style.width = `${width}px`; canvas.style.height = `${height}px`;
|
||||
canvas.width = width * dpr; canvas.height = actualHeight * dpr;
|
||||
canvas.style.width = `${width}px`; canvas.style.height = `${actualHeight}px`;
|
||||
const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
return { ctx, width, height };
|
||||
ctx.clearRect(0, 0, width, actualHeight);
|
||||
return { ctx, width, height:actualHeight };
|
||||
}
|
||||
|
||||
function drawEmptyChart(canvas, height = 340) {
|
||||
if (!canvas) return;
|
||||
const { ctx, width } = prepareCanvas(canvas, height);
|
||||
const prepared = prepareCanvas(canvas, height);
|
||||
const { ctx, width } = prepared;
|
||||
height = prepared.height;
|
||||
ctx.fillStyle = cssColor('--muted', '#888'); ctx.font = '13px system-ui'; ctx.textAlign = 'center';
|
||||
ctx.fillText(tr('history.noData'), width / 2, height / 2);
|
||||
const wrap = canvas.parentElement;
|
||||
@@ -392,7 +441,9 @@ function drawLineChart(canvas, series, rows, { height = 340, minValue = null, ma
|
||||
const visibleSeries = series.filter((item, index) => !isChartSeriesHidden(canvas.id, item, index));
|
||||
if (!rows.length || !visibleSeries.length) return drawEmptyChart(canvas, height);
|
||||
const sortedRows = [...rows].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||
const { ctx, width } = prepareCanvas(canvas, height);
|
||||
const prepared = prepareCanvas(canvas, height);
|
||||
const { ctx, width } = prepared;
|
||||
height = prepared.height;
|
||||
const text = cssColor('--muted', '#888'), grid = cssColor('--grid', '#333');
|
||||
const pad = { left: 54, right: 20, top: 20, bottom: 42 };
|
||||
const allValues = [];
|
||||
@@ -445,10 +496,15 @@ 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-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></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' : ''}>−</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>`;
|
||||
}
|
||||
|
||||
function historySeriesColor(index) {
|
||||
return cssColor(HISTORY_COLORS[index % HISTORY_COLORS.length], `hsl(${(index * 67) % 360} 68% 55%)`);
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') closeChartPreview(document.querySelector('.history-chart-card.chart-fullscreen-fallback'));
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ const app = {
|
||||
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {},
|
||||
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
|
||||
flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false,
|
||||
flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, flowZoom: 1,
|
||||
};
|
||||
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
|
||||
|
||||
|
||||
+7
-7
@@ -51,7 +51,7 @@ function deviceCard(device, detailed = false) {
|
||||
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}"${locked}>${esc(tr('devices.turbo'))}</button>`}
|
||||
</div>
|
||||
${detailed ? deviceFeaturePanel(device) : ''}
|
||||
${detailed ? `<div class="card-footer">${networkInfo}<div class="card-menu"><button data-action="poll" data-device="${esc(device.id)}">${esc(tr('actions.read'))}</button><button data-action="rename-device" data-device="${esc(device.id)}">${esc(tr('devices.nameProtocol'))}</button>${device.simulated ? '' : `<button data-action="bind" data-device="${esc(device.id)}">${esc(tr('actions.bind'))}</button>`}<button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></div>` : ''}
|
||||
${detailed ? `<div class="card-footer">${networkInfo}<div class="card-menu"><button data-action="poll" data-device="${esc(device.id)}">${esc(tr('actions.read'))}</button><button data-action="rename-device" data-device="${esc(device.id)}">${esc(tr('devices.nameProtocol'))}</button>${device.simulated ? '' : `<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></div></div>` : ''}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -307,7 +307,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><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></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'))}">•••</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>`;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,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><button class="danger" data-action="delete-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></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>` : ''}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -453,9 +453,9 @@ function renderSchedules() {
|
||||
$('#scheduleList').innerHTML = app.schedules.length ? app.schedules.map(item => {
|
||||
const zone = app.zones.find(z => z.id === item.zone_id);
|
||||
const days = item.weekdays.map(day => dayNames[day - 1]).join(', ');
|
||||
return `<article class="list-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('common.enabled') : tr('common.disabled'))}</span></div>
|
||||
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><button class="danger" data-action="delete-schedule" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button>`}</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'))}">•••</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>`;
|
||||
}
|
||||
|
||||
@@ -467,9 +467,9 @@ function renderAutomations() {
|
||||
const targetName = actionZone?.name || actionGroup?.name || actionDevice?.name || tr('common.noDevice');
|
||||
const mode = item.action.mode === 'auto' && (actionGroup || actionZone) ? tr('groups.followHouse') : (item.action.mode ? modeLabel(item.action.mode) : '—');
|
||||
const preset = item.action_zone_preset ? zonePresetLabel(item.action_zone_preset) : (item.action_preset ? zonePresetLabel(item.action_preset) : '—');
|
||||
return `<article class="list-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>
|
||||
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><button class="danger" data-action="delete-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button>`}</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'))}">•••</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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@ document.addEventListener('click', async event => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (button.dataset.chartFullscreen) {
|
||||
await toggleChartFullscreen(button.dataset.chartFullscreen);
|
||||
return;
|
||||
}
|
||||
if (button.dataset.chartZoom) {
|
||||
const id = button.dataset.chartId;
|
||||
const current = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
||||
|
||||
+105
-18
@@ -97,10 +97,10 @@ function flowDefaultConfig(kind) {
|
||||
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' : ''}">
|
||||
<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('common.disabled'))}</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('common.enabled') : tr('common.disabled'))}</button>`}</div>
|
||||
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="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 data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></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>
|
||||
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
@@ -150,13 +150,16 @@ function openFlowEditor(id = '', { push = true } = {}) {
|
||||
const flow = id ? app.flows.find(item => item.id === id) : null;
|
||||
if (id && !flow) return toast(tr('flow.notFound'), true);
|
||||
app.flowDraft = flowDraftFrom(flow);
|
||||
app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false;
|
||||
app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; app.flowZoom = 1;
|
||||
app.flowNameEditing = !flow;
|
||||
renderFlowNameMode();
|
||||
$('#flowEnabled').checked = app.flowDraft.enabled !== false;
|
||||
$('#flowEditor').hidden = false;
|
||||
document.body.classList.add('flow-editor-open');
|
||||
$('#flowNaturalPreview')?.classList.remove('is-expanded');
|
||||
$('#flowNaturalPreview .flow-preview-toggle')?.setAttribute('aria-expanded', 'false');
|
||||
renderFlowEditor();
|
||||
requestAnimationFrame(() => { if (window.matchMedia('(max-width: 760px)').matches && app.flowDraft?.nodes?.length) fitFlowToView({ maxZoom: .9 }); });
|
||||
startFlowSharedInputValueRefresh();
|
||||
if (push) updateBrowserUrl(`/flows/${flow?.id || 'new'}`);
|
||||
}
|
||||
@@ -164,6 +167,8 @@ function openFlowEditor(id = '', { push = true } = {}) {
|
||||
function closeFlowEditor({ push = true, force = false } = {}) {
|
||||
if (!force && app.flowDirty && !confirm(tr('confirm.discardChanges'))) return false;
|
||||
stopFlowSharedInputValueRefresh();
|
||||
const mobileActions = $('#flowEditorActionsDialog'); if (mobileActions?.open) mobileActions.close();
|
||||
const blockDialog = $('#flowBlockDialog'); if (blockDialog?.open) blockDialog.close();
|
||||
$('#flowEditor').hidden = true; document.body.classList.remove('flow-editor-open');
|
||||
app.flowDraft = null; app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; app.flowNameEditing = false;
|
||||
if (push) updateBrowserUrl('/flows');
|
||||
@@ -346,6 +351,65 @@ function flowOperatorLabel(op) { return ({ lt: '<', lte: '≤', gt: '>', gte: '
|
||||
function flowDurationLabel(seconds) { const value = Number(seconds || 0); return value >= 60 && value % 60 === 0 ? `${value / 60} min` : `${value} s`; }
|
||||
function flowNodeById(id) { return app.flowDraft?.nodes?.find(node => node.id === id); }
|
||||
|
||||
function renderFlowEnabledLabel() {
|
||||
const input = $('#flowEnabled'), label = $('#flowEnabledLabel');
|
||||
if (input && label) label.textContent = tr(input.checked ? 'flow.enabledState' : 'flow.disabledState');
|
||||
}
|
||||
|
||||
function renderFlowSaveStatus() {
|
||||
const status = $('#flowSaveStatus');
|
||||
if (!status || !app.flowDraft) return;
|
||||
const isNew = !app.flowDraft.id;
|
||||
status.textContent = app.flowDirty ? tr('flow.unsavedChanges') : isNew ? tr('flow.notSavedYet') : tr('flow.savedState');
|
||||
status.classList.toggle('is-dirty', app.flowDirty || isNew);
|
||||
}
|
||||
|
||||
function setFlowZoom(value, { render = true } = {}) {
|
||||
const next = clamp(Number(value) || 1, .45, 1.35);
|
||||
app.flowZoom = Math.round(next * 20) / 20;
|
||||
const canvas = $('#flowCanvas');
|
||||
if (canvas) canvas.style.zoom = String(app.flowZoom);
|
||||
const label = $('#flowZoomLabel');
|
||||
if (label) label.textContent = `${Math.round(app.flowZoom * 100)}%`;
|
||||
if (render) requestAnimationFrame(renderFlowEdges);
|
||||
}
|
||||
|
||||
function fitFlowToView({ maxZoom = 1 } = {}) {
|
||||
const workspace = $('#flowWorkspace');
|
||||
const nodes = app.flowDraft?.nodes || [];
|
||||
if (!workspace || !nodes.length) { setFlowZoom(1); if (workspace) workspace.scrollTo({ left:0, top:0, behavior:'smooth' }); return; }
|
||||
const width = 170, height = 96, pad = 56;
|
||||
const minX = Math.max(0, Math.min(...nodes.map(node => Number(node.x || 0))) - pad);
|
||||
const minY = Math.max(0, Math.min(...nodes.map(node => Number(node.y || 0))) - pad);
|
||||
const maxX = Math.max(...nodes.map(node => Number(node.x || 0) + width)) + pad;
|
||||
const maxY = Math.max(...nodes.map(node => Number(node.y || 0) + height)) + pad;
|
||||
const contentW = Math.max(1, maxX - minX), contentH = Math.max(1, maxY - minY);
|
||||
const availableW = Math.max(220, workspace.clientWidth - 24), availableH = Math.max(180, workspace.clientHeight - 24);
|
||||
const zoom = clamp(Math.min(availableW / contentW, availableH / contentH, maxZoom), .45, 1.35);
|
||||
setFlowZoom(zoom);
|
||||
requestAnimationFrame(() => workspace.scrollTo({ left:Math.max(0, minX * app.flowZoom - 12), top:Math.max(0, minY * app.flowZoom - 12), behavior:'smooth' }));
|
||||
}
|
||||
|
||||
function renderFlowBlockLibrary(filter = '') {
|
||||
const host = $('#flowBlockLibrary'); if (!host) return;
|
||||
const query = String(filter || '').trim().toLocaleLowerCase(locale());
|
||||
const groups = $$('.flow-palette .flow-palette-group');
|
||||
host.innerHTML = groups.map(group => {
|
||||
const title = group.querySelector(':scope > span')?.textContent?.trim() || '';
|
||||
const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query));
|
||||
if (!buttons.length) return '';
|
||||
const categoryClass = [...group.classList].find(name => name.startsWith('flow-palette-') && name !== 'flow-palette-group') || '';
|
||||
return `<section class="flow-block-library-group ${esc(categoryClass)}"><h3>${esc(title)}</h3><div>${buttons.map(button => `<button type="button" class="secondary" data-flow-add="${esc(button.dataset.flowAdd)}">${esc(button.textContent.trim())}</button>`).join('')}</div></section>`;
|
||||
}).join('') || `<div class="empty compact"><strong>${esc(tr('flow.noBlocksFound'))}</strong><span>${esc(tr('flow.noBlocksFoundHint'))}</span></div>`;
|
||||
}
|
||||
|
||||
function openFlowBlockLibrary() {
|
||||
const search = $('#flowBlockSearch'); if (search) search.value = '';
|
||||
renderFlowBlockLibrary('');
|
||||
$('#flowBlockDialog')?.showModal();
|
||||
requestAnimationFrame(() => search?.focus());
|
||||
}
|
||||
|
||||
function renderFlowEditor() {
|
||||
const draft = app.flowDraft; if (!draft) return;
|
||||
const nodesHost = $('#flowNodes');
|
||||
@@ -360,7 +424,7 @@ function renderFlowEditor() {
|
||||
}).join('');
|
||||
$('#flowEmptyHint').hidden = draft.nodes.length > 0;
|
||||
const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = (app.flowSelectedNodeIds || []).length ? tr('flow.selectedCount', { count:(app.flowSelectedNodeIds || []).length }) : '';
|
||||
renderFlowNameMode(); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); refreshFlowSharedInputCurrentValues();
|
||||
renderFlowNameMode(); renderFlowEnabledLabel(); setFlowZoom(app.flowZoom || 1, { render:false }); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); renderFlowSaveStatus(); refreshFlowSharedInputCurrentValues();
|
||||
const status = $('#flowCompileStatus');
|
||||
status.textContent = draft.draft ? tr('flow.draftStatus') : tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 });
|
||||
status.classList.toggle('flow-draft-badge', draft.draft === true);
|
||||
@@ -368,13 +432,13 @@ function renderFlowEditor() {
|
||||
|
||||
function renderFlowEdges() {
|
||||
const svg = $('#flowEdges'), workspace = $('#flowWorkspace'); if (!svg || !workspace || !app.flowDraft) return;
|
||||
const rect = workspace.getBoundingClientRect();
|
||||
svg.setAttribute('viewBox', `0 0 ${Math.max(workspace.clientWidth, workspace.scrollWidth)} ${Math.max(workspace.clientHeight, workspace.scrollHeight)}`);
|
||||
const rect = workspace.getBoundingClientRect(), zoom = app.flowZoom || 1;
|
||||
svg.setAttribute('viewBox', '0 0 2400 1500');
|
||||
svg.innerHTML = app.flowDraft.edges.map(edge => {
|
||||
const from = $(`[data-flow-node="${CSS.escape(edge.from)}"]`), to = $(`[data-flow-node="${CSS.escape(edge.to)}"]`); if (!from || !to) return '';
|
||||
const a = from.getBoundingClientRect(), b = to.getBoundingClientRect();
|
||||
const x1 = a.right - rect.left + workspace.scrollLeft - 2, y1 = a.top - rect.top + workspace.scrollTop + a.height / 2;
|
||||
const x2 = b.left - rect.left + workspace.scrollLeft + 2, y2 = b.top - rect.top + workspace.scrollTop + b.height / 2;
|
||||
const x1 = (a.right - rect.left + workspace.scrollLeft) / zoom - 2, y1 = (a.top - rect.top + workspace.scrollTop + a.height / 2) / zoom;
|
||||
const x2 = (b.left - rect.left + workspace.scrollLeft) / zoom + 2, y2 = (b.top - rect.top + workspace.scrollTop + b.height / 2) / zoom;
|
||||
const bend = Math.max(55, Math.abs(x2 - x1) * .45);
|
||||
return `<path data-flow-edge="${esc(edge.id)}" d="M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}"/>`;
|
||||
}).join('');
|
||||
@@ -401,7 +465,8 @@ function sharedFlowReferenceComparisonFields(item, config) {
|
||||
|
||||
function renderFlowInspector() {
|
||||
const host = $('#flowInspector'), node = flowNodeById(app.flowSelectedNodeId); if (!host) return;
|
||||
if (!node) { host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.selectBlock'))}</strong><span>${esc(tr('flow.selectBlockHint'))}</span></div>`; return; }
|
||||
host.classList.toggle('has-selection', Boolean(node));
|
||||
if (!node) { host.classList.remove('is-expanded'); host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.selectBlock'))}</strong><span>${esc(tr('flow.selectBlockHint'))}</span></div>`; return; }
|
||||
const c = node.config || {}, meta = FLOW_NODE_META[node.kind] || { title: node.kind };
|
||||
let fields = '';
|
||||
if (node.kind === 'weekday') fields = `<div class="flow-day-grid">${[1,2,3,4,5,6,7].map(day => `<label><input type="checkbox" data-flow-config="days" value="${day}" ${(c.days || []).includes(day) ? 'checked' : ''}><span>${esc(tr(`day.${day}`))}</span></label>`).join('')}</div>`;
|
||||
@@ -440,8 +505,8 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto','comfort','sleep','away','custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
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><label><span>entity_id</span><input data-flow-config="entity_id" 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><button type="button" class="danger" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
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" 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>`;
|
||||
}
|
||||
|
||||
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>`; }
|
||||
@@ -540,8 +605,12 @@ function flowUpstreamNodes(id) {
|
||||
function addFlowNode(kind) {
|
||||
if (!app.flowDraft || !FLOW_NODE_META[kind]) return;
|
||||
const count = app.flowDraft.nodes.length;
|
||||
const node = { id: newFlowId('node'), kind, x: 80 + (count % 5) * 190, y: 70 + Math.floor(count / 5) * 130, config: flowDefaultConfig(kind) };
|
||||
const workspace = $('#flowWorkspace');
|
||||
const viewportX = workspace ? (workspace.scrollLeft / (app.flowZoom || 1)) + 48 : 80;
|
||||
const viewportY = workspace ? (workspace.scrollTop / (app.flowZoom || 1)) + 54 : 70;
|
||||
const node = { id: newFlowId('node'), kind, x: Math.max(36, viewportX + (count % 3) * 24), y: Math.max(36, viewportY + (count % 3) * 24), config: flowDefaultConfig(kind) };
|
||||
app.flowDraft.nodes.push(node); app.flowSelectedNodeId = node.id; app.flowSelectedNodeIds = [node.id]; app.flowDirty = true; renderFlowEditor();
|
||||
requestAnimationFrame(() => $(`[data-flow-node="${CSS.escape(node.id)}"]`)?.scrollIntoView({ block:'center', inline:'center', behavior:'smooth' }));
|
||||
}
|
||||
function removeFlowNode(id) {
|
||||
if (!app.flowDraft) return;
|
||||
@@ -1071,7 +1140,7 @@ document.addEventListener('pointerdown', event => {
|
||||
});
|
||||
document.addEventListener('pointermove', event => {
|
||||
if (!flowDrag || !app.flowDraft) return;
|
||||
let dx = event.clientX - flowDrag.x, dy = event.clientY - flowDrag.y;
|
||||
let dx = (event.clientX - flowDrag.x) / (app.flowZoom || 1), dy = (event.clientY - flowDrag.y) / (app.flowZoom || 1);
|
||||
const minLeft = Math.min(...flowDrag.starts.map(item => item.left));
|
||||
const minTop = Math.min(...flowDrag.starts.map(item => item.top));
|
||||
dx = Math.max(dx, 12 - minLeft); dy = Math.max(dy, 12 - minTop);
|
||||
@@ -1080,11 +1149,13 @@ document.addEventListener('pointermove', event => {
|
||||
node.x = start.left + dx; node.y = start.top + dy;
|
||||
const el = $(`[data-flow-node="${CSS.escape(node.id)}"]`); if (el) { el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; }
|
||||
});
|
||||
app.flowDirty = true; renderFlowEdges();
|
||||
app.flowDirty = true; renderFlowSaveStatus(); renderFlowEdges();
|
||||
});
|
||||
document.addEventListener('pointerup', () => { flowDrag = null; });
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
const flowCard = event.target.closest?.('[data-flow-card-id]');
|
||||
if (flowCard && event.target === flowCard && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openFlowEditor(flowCard.dataset.flowCardId); return; }
|
||||
if ($('#flowEditor')?.hidden || !app.flowDraft) return;
|
||||
if (event.target.closest?.('input,select,textarea,[contenteditable="true"]')) return;
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { event.preventDefault(); selectAllFlowNodes(); return; }
|
||||
@@ -1092,8 +1163,10 @@ document.addEventListener('keydown', event => {
|
||||
});
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
const flowCard = event.target.closest?.('[data-flow-card-id]');
|
||||
if (flowCard && !event.target.closest?.('button,summary,details,input,select,textarea,a,label')) { openFlowEditor(flowCard.dataset.flowCardId); return; }
|
||||
const edge = event.target.closest?.('[data-flow-edge]'); if (edge) { removeFlowEdge(edge.dataset.flowEdge); return; }
|
||||
const add = event.target.closest?.('[data-flow-add]'); if (add) { addFlowNode(add.dataset.flowAdd); return; }
|
||||
const add = event.target.closest?.('[data-flow-add]'); if (add) { addFlowNode(add.dataset.flowAdd); const dialog = add.closest?.('#flowBlockDialog'); if (dialog?.open) dialog.close(); return; }
|
||||
const templateCategory = event.target.closest?.('[data-flow-template-category]'); if (templateCategory) { renderFlowPresetBrowser(templateCategory.dataset.flowTemplateCategory); return; }
|
||||
const templateFavorite = event.target.closest?.('[data-flow-template-favorite]'); if (templateFavorite) { toggleFlowPresetFavorite(templateFavorite.dataset.flowTemplateFavorite); return; }
|
||||
const templatePreview = event.target.closest?.('[data-flow-template-preview]'); if (templatePreview) { previewFlowTemplate(templatePreview.dataset.flowTemplatePreview); return; }
|
||||
@@ -1103,11 +1176,24 @@ document.addEventListener('click', event => {
|
||||
const input = event.target.closest?.('[data-flow-input]'); if (input) { if (app.flowConnectFrom) connectFlowNodes(app.flowConnectFrom, input.dataset.flowInput); return; }
|
||||
const node = event.target.closest?.('[data-flow-node]'); if (node) { if (!(app.flowSelectedNodeIds || []).includes(node.dataset.flowNode)) app.flowSelectedNodeIds = [node.dataset.flowNode]; app.flowSelectedNodeId = node.dataset.flowNode; renderFlowEditor(); return; }
|
||||
const actionButton = event.target.closest?.('[data-action]'); const action = actionButton?.dataset.action;
|
||||
const mobileActionsDialog = actionButton?.closest?.('#flowEditorActionsDialog');
|
||||
if (mobileActionsDialog?.open && action !== 'flow-mobile-actions') mobileActionsDialog.close();
|
||||
if (action === 'new-flow') openFlowEditor();
|
||||
else if (action === 'edit-flow') openFlowEditor(actionButton.dataset.id);
|
||||
else if (action === 'delete-flow') deleteFlow(actionButton.dataset.id);
|
||||
else if (action === 'toggle-flow-enabled') toggleFlowEnabled(actionButton.dataset.id, actionButton.dataset.value === 'true');
|
||||
else if (action === 'save-flow') saveFlow();
|
||||
else if (action === 'flow-mobile-actions') $('#flowEditorActionsDialog')?.showModal();
|
||||
else if (action === 'flow-add-block') openFlowBlockLibrary();
|
||||
else if (action === 'flow-zoom-out') setFlowZoom((app.flowZoom || 1) - .1);
|
||||
else if (action === 'flow-zoom-in') setFlowZoom((app.flowZoom || 1) + .1);
|
||||
else if (action === 'flow-fit') fitFlowToView();
|
||||
else if (action === 'flow-preview-toggle') {
|
||||
const preview = $('#flowNaturalPreview');
|
||||
const expanded = preview?.classList.toggle('is-expanded') || false;
|
||||
actionButton.setAttribute('aria-expanded', String(expanded));
|
||||
}
|
||||
else if (action === 'flow-toggle-inspector') $('#flowInspector')?.classList.toggle('is-expanded');
|
||||
else if (action === 'import-flow') $('#flowImportFile')?.click();
|
||||
else if (action === 'export-flow') exportFlow();
|
||||
else if (action === 'flow-templates') openFlowTemplates();
|
||||
@@ -1134,7 +1220,8 @@ document.addEventListener('change', event => {
|
||||
});
|
||||
$('#flowImportFile')?.addEventListener('change', event => importFlowFile(event.target.files?.[0]));
|
||||
$('#flowTemplateSearch')?.addEventListener('input', event => { flowPresetSearch = event.target.value || ''; renderFlowPresetBrowser(flowPresetActiveCategory); });
|
||||
$('#flowName')?.addEventListener('input', () => { if (app.flowDraft) app.flowDirty = true; });
|
||||
$('#flowBlockSearch')?.addEventListener('input', event => renderFlowBlockLibrary(event.target.value));
|
||||
$('#flowName')?.addEventListener('input', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowSaveStatus(); } });
|
||||
$('#flowName')?.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter') { event.preventDefault(); commitFlowName(); }
|
||||
else if (event.key === 'Escape') { event.preventDefault(); $('#flowName').value = app.flowDraft?.name || ''; app.flowNameEditing = false; renderFlowNameMode(); }
|
||||
@@ -1143,5 +1230,5 @@ $('#flowName')?.addEventListener('blur', () => {
|
||||
if (!app.flowDraft || !app.flowNameEditing) return;
|
||||
if ($('#flowName').value.trim()) commitFlowName();
|
||||
});
|
||||
$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) app.flowDirty = true; });
|
||||
$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowEnabledLabel(); renderFlowSaveStatus(); } });
|
||||
window.addEventListener('resize', () => { if (!$('#flowEditor')?.hidden) renderFlowEdges(); });
|
||||
|
||||
@@ -19,7 +19,7 @@ function showView(name, { push = true, scroll = true } = {}) {
|
||||
if (name !== 'flows' && !$('#flowEditor')?.hidden) closeFlowEditor({ push: false, force: true });
|
||||
app.currentView = name;
|
||||
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
|
||||
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['groups', 'flows', 'schedules', 'automations', 'simulation', 'night', 'homeassistant', 'settings', 'logs'].includes(name))));
|
||||
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['devices', 'groups', 'schedules', 'automations', 'simulation', 'night', 'homeassistant', 'settings', 'logs'].includes(name))));
|
||||
if (push) updateBrowserUrl(pathForView(name));
|
||||
if (scroll) window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
if (name === 'dashboard') setDashboardTab(app.dashboardTab, { scroll: false });
|
||||
|
||||
+17
-1
@@ -8,6 +8,18 @@ function renderAccessTokens() {
|
||||
</div>`).join('') : `<div class="empty compact"><strong>${esc(tr('settings.noTokens'))}</strong>${esc(tr('settings.noTokensHint'))}</div>`;
|
||||
}
|
||||
|
||||
function metricHaEntities() {
|
||||
return new Set([
|
||||
app.settings?.home_assistant?.outdoor_entity_id,
|
||||
...app.zones.filter(zone => ['home_assistant', 'combined'].includes(zone.sensor_source)).map(zone => zone.ha_entity_id),
|
||||
...app.historyData.sensors.map(row => row.entity_id),
|
||||
].filter(Boolean));
|
||||
}
|
||||
|
||||
function flowHaEntities() {
|
||||
return new Set((app.flowSharedInputs || []).map(item => item?.config?.entity_id).filter(Boolean));
|
||||
}
|
||||
|
||||
function knownHaEntities() {
|
||||
return [...new Set([
|
||||
...Object.keys(app.sensorAliases || {}),
|
||||
@@ -22,7 +34,11 @@ function knownHaEntities() {
|
||||
function renderSensorAliases() {
|
||||
const host = $('#sensorAliasList'); if (!host) return;
|
||||
const entities = knownHaEntities();
|
||||
host.innerHTML = entities.length ? entities.map(entity => `<div class="sensor-alias-row"><span class="mono" title="${esc(entity)}">${esc(entity)}</span><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>`).join('') : `<div class="empty compact">${esc(tr('settings.noSensorAliases'))}</div>`;
|
||||
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>`;
|
||||
}).join('') : `<div class="empty compact">${esc(tr('settings.noSensorAliases'))}</div>`;
|
||||
}
|
||||
|
||||
function flowSharedInputKinds() {
|
||||
|
||||
+337
-2
@@ -35,7 +35,7 @@
|
||||
--grid: rgba(255, 255, 255, .07);
|
||||
--scroll-thumb: #444444;
|
||||
--scroll-thumb-hover: #575757;
|
||||
--radius: 22px;
|
||||
--radius: 16px;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
@@ -6180,7 +6180,7 @@ body.flow-editor-open { overflow:hidden; }
|
||||
.flow-editor-bar { min-height:76px; display:flex; align-items:center; justify-content:space-between; gap:20px; padding:10px 18px; border-bottom:1px solid var(--line); background:var(--surface); }
|
||||
.flow-editor-title { display:flex; align-items:center; gap:12px; min-width:0; }
|
||||
.flow-editor-title > div { display:grid; gap:2px; min-width:0; }
|
||||
.flow-name-wrap { min-width:280px; min-height:36px; display:flex; align-items:center; }
|
||||
.flow-name-wrap { min-width:280px; min-height:36px; display:flex; align-items:center; gap:8px; }
|
||||
.flow-name-view { display:flex; align-items:center; gap:6px; min-width:0; }
|
||||
.flow-name-view[hidden] { display:none !important; }
|
||||
.flow-name-text { appearance:none; border:0; background:transparent; color:var(--text); font:inherit; font-weight:750; font-size:1.12rem; line-height:1.25; padding:4px 2px; min-width:0; max-width:min(52vw,620px); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:text; text-align:left; }
|
||||
@@ -6426,3 +6426,338 @@ body.flow-editor-open { overflow:hidden; }
|
||||
.flow-template-preview-node-trigger { border-top:3px solid var(--warning); }
|
||||
.flow-template-preview-node-time, .flow-template-preview-node-timeop { border-top:3px solid var(--blue); }
|
||||
.flow-template-preview-node-haaction { border-top:3px solid var(--accent); }
|
||||
|
||||
|
||||
/* Compact Flow state control and mobile action entry point. */
|
||||
.flow-enabled { white-space:nowrap; }
|
||||
.flow-enabled input { width:16px; min-height:16px; accent-color:var(--accent); }
|
||||
.flow-mobile-actions-button { display:none; }
|
||||
.flow-inspector-head-actions { display:flex; align-items:center; gap:7px; }
|
||||
.flow-inspector-close { display:none; width:34px; height:34px; padding:0; font-size:1.1rem; }
|
||||
|
||||
/* Clarify which HA aliases are metric sources and which are only Flow references. */
|
||||
.sensor-alias-entity { min-width:0; display:flex; align-items:center; gap:7px; }
|
||||
.sensor-alias-entity > .mono { min-width:0; overflow:hidden; color:var(--muted); font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.sensor-source-badges { display:flex; flex:0 0 auto; gap:4px; }
|
||||
.sensor-source-badge { display:inline-flex; align-items:center; min-height:20px; padding:2px 6px; border:1px solid var(--line); border-radius:999px; font-size:.64rem; font-weight:750; line-height:1; white-space:nowrap; }
|
||||
.sensor-source-badge.metrics { border-color:var(--accent-border); background:var(--accent-soft); color:var(--accent); }
|
||||
.sensor-source-badge.flow { background:var(--surface); color:var(--muted); }
|
||||
|
||||
/* UX refresh: clearer hierarchy, safer destructive actions and touch-first layouts. */
|
||||
:root { --radius-small:10px; --radius-medium:14px; }
|
||||
|
||||
.device-card,.panel,.list-card { border-radius:var(--radius); }
|
||||
button { border-radius:11px; }
|
||||
.metric { border-radius:14px; }
|
||||
.lead { max-width:780px; line-height:1.55; }
|
||||
.section-heading h1 { margin-bottom:0; }
|
||||
|
||||
.automation-tabs {
|
||||
display:flex; align-items:center; gap:5px; width:max-content; max-width:100%;
|
||||
margin:-2px 0 14px; padding:5px; overflow-x:auto; border:1px solid var(--line);
|
||||
border-radius:14px; background:var(--surface); scrollbar-width:none;
|
||||
}
|
||||
.automation-tabs::-webkit-scrollbar { display:none; }
|
||||
.automation-tabs button { flex:0 0 auto; min-height:38px; padding:8px 12px; color:var(--muted); background:transparent; font-size:12px; font-weight:750; }
|
||||
.automation-tabs button.active { color:var(--accent); background:var(--accent-soft); box-shadow:inset 0 0 0 1px var(--accent-border); }
|
||||
|
||||
.mobile-nav-only { display:block; }
|
||||
.desktop-nav-only { display:none; }
|
||||
.nav-section-label,.menu-section-label { color:var(--muted); font-size:9px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; }
|
||||
.menu-section-label { display:block; padding:14px 9px 5px; }
|
||||
.more-menu-list .menu-section-label:first-child { padding-top:5px; }
|
||||
|
||||
.card-menu { align-items:center; }
|
||||
.card-primary-action { font-weight:700; }
|
||||
.card-overflow { position:relative; flex:0 0 auto; }
|
||||
.card-overflow summary {
|
||||
display:grid; place-items:center; width:38px; height:38px; border:1px solid var(--line);
|
||||
border-radius:10px; color:var(--muted); background:var(--surface-muted); cursor:pointer;
|
||||
list-style:none; font-size:16px; font-weight:800; letter-spacing:.08em;
|
||||
}
|
||||
.card-overflow summary::-webkit-details-marker { display:none; }
|
||||
.card-overflow[open] summary { color:var(--text); border-color:var(--line-strong); background:var(--surface-2); }
|
||||
.card-overflow-menu {
|
||||
position:absolute; z-index:25; right:0; bottom:calc(100% + 7px); min-width:150px; padding:6px;
|
||||
border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:0 14px 34px rgba(0,0,0,.28);
|
||||
}
|
||||
.card-overflow-menu button { width:100%; text-align:left; }
|
||||
.card-overflow-menu .danger { background:var(--danger-soft); }
|
||||
|
||||
.flow-save-status { display:inline-flex; align-items:center; min-height:24px; color:var(--muted); font-size:.72rem; font-weight:750; white-space:nowrap; }
|
||||
.flow-save-status::before { content:""; width:6px; height:6px; margin-right:6px; border-radius:50%; background:var(--accent); }
|
||||
.flow-save-status.is-dirty { color:var(--warning); }
|
||||
.flow-save-status.is-dirty::before { background:var(--warning); }
|
||||
.flow-canvas { position:relative; width:2400px; height:1500px; transform-origin:0 0; }
|
||||
.flow-canvas .flow-nodes,.flow-canvas .flow-edges { width:2400px; height:1500px; }
|
||||
.flow-workspace-toolbar { justify-content:space-between; }
|
||||
.flow-workspace-primary-actions,.flow-zoom-controls { display:flex; align-items:center; gap:7px; min-width:0; }
|
||||
.flow-add-block-button { display:none; align-items:center; justify-content:center; gap:5px; }
|
||||
.flow-zoom-controls .icon-button { width:32px; height:32px; min-height:32px; padding:0; font-size:17px; }
|
||||
.flow-zoom-controls > span { min-width:44px; color:var(--muted); font-size:.72rem; font-weight:800; text-align:center; font-variant-numeric:tabular-nums; }
|
||||
.flow-fit-button { min-height:32px; padding:6px 9px; font-size:.75rem; }
|
||||
.flow-inspector-expand { display:none; width:34px; height:34px; padding:0; }
|
||||
|
||||
.flow-block-dialog { width:min(620px,calc(100% - 24px)); }
|
||||
.flow-block-dialog .dialog-head { padding:16px 16px 8px; }
|
||||
.flow-block-search { padding:4px 16px 10px; }
|
||||
.flow-block-library { display:grid; gap:14px; padding:0 16px 18px; }
|
||||
.flow-block-library-group { padding-left:10px; border-left:3px solid var(--line-strong); }
|
||||
.flow-block-library-group.flow-palette-trigger { border-left-color:var(--warning); }
|
||||
.flow-block-library-group.flow-palette-time { border-left-color:var(--blue); }
|
||||
.flow-block-library-group.flow-palette-timeop { border-left-color:var(--info); }
|
||||
.flow-block-library-group.flow-palette-sensor { border-left-color:var(--teal); }
|
||||
.flow-block-library-group.flow-palette-logic { border-left-color:var(--purple); }
|
||||
.flow-block-library-group.flow-palette-action { border-left-color:var(--orange); }
|
||||
.flow-block-library-group.flow-palette-haaction { border-left-color:var(--accent); }
|
||||
.flow-block-library-group h3 { margin:0 0 7px; color:var(--muted); font-size:.72rem; letter-spacing:.07em; text-transform:uppercase; }
|
||||
.flow-block-library-group > div { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; }
|
||||
.flow-block-library-group button { min-height:42px; text-align:left; }
|
||||
|
||||
/* Port stays visually small but gets a comfortable touch target. */
|
||||
.flow-editor .flow-port { width:16px; height:16px; margin-top:-8px; }
|
||||
.flow-editor .flow-port-in { left:-9px; }
|
||||
.flow-editor .flow-port-out { right:-9px; }
|
||||
.flow-editor .flow-port::after { content:""; position:absolute; inset:-11px; border-radius:50%; }
|
||||
|
||||
.flow-preview-toggle { display:none; }
|
||||
.flow-preview-details { display:flex; justify-content:space-between; gap:24px; align-items:center; width:100%; min-width:0; }
|
||||
|
||||
@media (min-width:900px) {
|
||||
.mobile-nav-only { display:none !important; }
|
||||
.desktop-nav-only { display:flex; }
|
||||
.bottom-nav {
|
||||
top:calc(var(--topbar-height,72px) + 14px); bottom:14px; left:max(14px,calc((100vw - 1440px)/2));
|
||||
display:flex; flex-direction:column; gap:2px; width:190px; max-height:calc(100dvh - var(--topbar-height,72px) - 28px);
|
||||
padding:10px; overflow-y:auto; border-radius:16px; scrollbar-width:thin;
|
||||
}
|
||||
.bottom-nav .nav-section-label { padding:10px 9px 4px; }
|
||||
.bottom-nav .nav-section-label:first-child { padding-top:3px; }
|
||||
.bottom-nav button { display:flex; align-items:center; justify-content:flex-start; gap:10px; min-height:39px; padding:8px 10px; border-radius:10px; text-align:left; }
|
||||
.bottom-nav button span { display:grid; place-items:center; flex:0 0 24px; width:24px; font-size:17px; }
|
||||
.bottom-nav button b { min-width:0; overflow:hidden; font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.bottom-nav button.active { background:var(--accent-soft); }
|
||||
main { width:min(1440px,100%); padding-left:222px; padding-right:24px; }
|
||||
}
|
||||
|
||||
@media (max-width:760px) {
|
||||
main { padding:18px 12px calc(94px + env(safe-area-inset-bottom)); }
|
||||
.desktop-nav-only { display:none !important; }
|
||||
.mobile-nav-only { display:grid; }
|
||||
.bottom-nav { grid-template-columns:repeat(5,minmax(0,1fr)); }
|
||||
.bottom-nav button { min-height:48px; }
|
||||
.bottom-nav button span { font-size:19px; }
|
||||
|
||||
.section-heading { gap:12px; margin:4px 0 14px; }
|
||||
.section-heading h1 { font-size:26px; }
|
||||
.section-heading > button,.section-heading > .section-actions,.section-heading > .section-heading-actions { width:100%; }
|
||||
.section-heading > button { min-height:44px; }
|
||||
.flow-page-actions,.section-heading-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; }
|
||||
.flow-page-actions button,.section-heading-actions button { width:100%; min-height:44px; }
|
||||
.automation-tabs { width:100%; margin-bottom:12px; scroll-snap-type:x proximity; }
|
||||
.automation-tabs button { flex:1 0 auto; scroll-snap-align:start; }
|
||||
|
||||
.device-card,.panel,.list-card { border-radius:14px; }
|
||||
.device-card { padding:15px; }
|
||||
.card-footer { gap:9px; }
|
||||
.card-menu { width:100%; justify-content:flex-end; }
|
||||
.card-menu > button { min-height:40px; }
|
||||
.card-primary-action { flex:1 1 auto; }
|
||||
.card-overflow summary { width:40px; height:40px; }
|
||||
|
||||
dialog:not(.auth-dialog) {
|
||||
inset:auto 0 0; width:100%; max-width:none; max-height:88dvh; margin:0;
|
||||
border-right:0; border-bottom:0; border-left:0; border-radius:18px 18px 0 0;
|
||||
overscroll-behavior:contain;
|
||||
}
|
||||
dialog:not(.auth-dialog) .dialog-head { position:sticky; top:0; z-index:5; padding-top:14px; background:var(--dialog-bg); }
|
||||
dialog:not(.auth-dialog) .dialog-head::before { content:""; position:absolute; top:6px; left:50%; width:38px; height:4px; border-radius:999px; background:var(--line-strong); transform:translateX(-50%); }
|
||||
.dialog-form { padding:14px 14px calc(14px + env(safe-area-inset-bottom)); }
|
||||
.dialog-form .form-actions { position:sticky; bottom:0; z-index:4; margin:6px -14px -14px; padding:10px 14px calc(10px + env(safe-area-inset-bottom)); border-top:1px solid var(--line); background:color-mix(in srgb,var(--dialog-bg) 96%,transparent); backdrop-filter:blur(12px); }
|
||||
.dialog-form .form-actions > button { flex:1 1 0; min-height:44px; }
|
||||
|
||||
.history-tabs { margin-inline:-12px; padding-inline:12px; scroll-padding-inline:12px; scroll-snap-type:x proximity; }
|
||||
.history-tabs button { min-height:40px; scroll-snap-align:start; }
|
||||
.history-toolbar-panel { padding:12px; }
|
||||
|
||||
.settings-mode-tabs { position:sticky; top:calc(var(--topbar-height,64px) + 6px); z-index:8; margin-inline:-4px; background:color-mix(in srgb,var(--bg) 94%,transparent); backdrop-filter:blur(12px); }
|
||||
.settings-save-bar { bottom:calc(74px + env(safe-area-inset-bottom)); margin-inline:-4px; padding:10px; }
|
||||
.settings-save-bar span { display:none; }
|
||||
.settings-save-bar button { width:100%; min-height:44px; }
|
||||
|
||||
/* Mobile Flow: compact header, canvas-first editor and bottom-sheet inspector. */
|
||||
.flow-editor-bar {
|
||||
display:grid; grid-template-columns:minmax(0,1fr) auto; grid-template-rows:auto auto;
|
||||
align-items:center; width:100%; gap:6px 8px; padding:7px 8px; overflow:hidden;
|
||||
}
|
||||
.flow-editor-title { grid-column:1; grid-row:1; width:100%; min-width:0; gap:8px; }
|
||||
.flow-editor-title > div { width:100%; min-width:0; }
|
||||
.flow-editor-title .eyebrow { display:none; }
|
||||
.flow-name-wrap { display:flex; width:100%; min-width:0; gap:6px; }
|
||||
.flow-name-view { min-width:0; flex:1 1 auto; }
|
||||
.flow-name-text { min-width:0; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.flow-editor-title input { width:100%; min-width:0; flex:1 1 auto; font-size:1rem; }
|
||||
.flow-editor-bar > .flow-save-button {
|
||||
grid-column:2; grid-row:1; align-self:stretch; min-width:0; min-height:38px;
|
||||
padding-inline:11px; white-space:nowrap;
|
||||
}
|
||||
.flow-editor-actions {
|
||||
grid-column:1 / -1; grid-row:2; display:flex; align-items:center; justify-content:space-between;
|
||||
width:100%; min-width:0; max-width:none; gap:8px; overflow:hidden;
|
||||
}
|
||||
.flow-editor-actions .flow-desktop-action,.flow-save-status,.flow-compile-status { display:none; }
|
||||
.flow-editor-actions .flow-enabled {
|
||||
display:flex; flex:0 1 auto; width:auto; min-width:0; min-height:34px;
|
||||
justify-content:flex-start; gap:7px; padding:0 4px; font-size:.78rem;
|
||||
}
|
||||
.flow-editor-actions .flow-enabled span { display:inline; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.flow-editor-actions .flow-mobile-actions-button {
|
||||
display:inline-flex; flex:0 0 auto; width:auto; min-width:96px; min-height:34px;
|
||||
margin-left:auto; padding:6px 12px; justify-content:center;
|
||||
}
|
||||
|
||||
.flow-editor-body { position:relative; display:flex; flex:1 1 auto; flex-direction:column; height:0; min-width:0; min-height:0; overflow:hidden; }
|
||||
.flow-workspace-wrap {
|
||||
order:1; display:grid; grid-template-rows:auto minmax(0,1fr) auto; flex:1 1 auto;
|
||||
width:100%; height:100%; min-width:0; min-height:0; overflow:hidden;
|
||||
}
|
||||
.flow-workspace-toolbar { display:flex; flex:0 0 auto; width:100%; min-width:0; min-height:48px; padding:6px 8px; gap:7px; justify-content:space-between; overflow:hidden; }
|
||||
.flow-workspace-primary-actions { display:flex; flex:1 1 auto; min-width:0; overflow:hidden; }
|
||||
.flow-add-block-button { display:inline-flex; min-width:0; max-width:128px; min-height:36px; padding:7px 10px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.flow-selection-action,#flowSelectionCount { display:none; }
|
||||
.flow-zoom-controls { display:flex; flex:0 0 auto; margin-left:auto; gap:4px; }
|
||||
.flow-zoom-controls .icon-button { width:34px; height:34px; min-height:34px; }
|
||||
.flow-zoom-controls > span { display:none; }
|
||||
.flow-fit-button { width:34px; min-width:34px; min-height:34px; padding:0; font-size:0; }
|
||||
.flow-fit-button::before { content:"⊡"; font-size:17px; line-height:1; }
|
||||
.flow-palette { display:none !important; }
|
||||
.flow-workspace { height:100%; min-width:0; min-height:0 !important; max-height:none; overflow:auto; overscroll-behavior:contain; touch-action:pan-x pan-y; }
|
||||
|
||||
.flow-natural-preview { display:block; flex:0 0 auto; min-height:0; max-height:none; padding:0; overflow:hidden; }
|
||||
.flow-preview-toggle {
|
||||
display:flex; align-items:center; justify-content:space-between; width:100%; min-height:38px;
|
||||
padding:7px 10px; border:0; border-radius:0; background:var(--surface); color:var(--text); text-align:left;
|
||||
}
|
||||
.flow-preview-toggle strong { font-size:.78rem; }
|
||||
.flow-preview-toggle-icon { color:var(--muted); font-size:1rem; transition:transform .15s ease; }
|
||||
.flow-preview-details { display:none; flex-direction:column; align-items:stretch; gap:7px; max-height:32dvh; padding:8px 10px 10px; overflow:auto; }
|
||||
.flow-natural-preview.is-expanded .flow-preview-details { display:flex; }
|
||||
.flow-natural-preview.is-expanded .flow-preview-toggle { border-bottom:1px solid var(--line); }
|
||||
.flow-natural-preview.is-expanded .flow-preview-toggle-icon { transform:rotate(180deg); }
|
||||
.flow-preview-item { gap:1px; }
|
||||
.flow-natural-preview strong { font-size:.78rem; }
|
||||
.flow-natural-preview span { font-size:.76rem; }
|
||||
|
||||
.flow-inspector {
|
||||
display:none; position:fixed; inset:auto 0 0; width:100%; max-height:56dvh;
|
||||
padding:22px 14px calc(14px + env(safe-area-inset-bottom)); overflow:auto;
|
||||
border:0; border-top:1px solid var(--line-strong); border-radius:18px 18px 0 0;
|
||||
z-index:95; box-shadow:0 -14px 34px rgba(0,0,0,.3); transition:max-height .18s ease;
|
||||
}
|
||||
.flow-inspector.has-selection { display:block; }
|
||||
.flow-inspector::before { content:""; position:absolute; top:7px; left:50%; width:38px; height:4px; border-radius:999px; background:var(--line-strong); transform:translateX(-50%); }
|
||||
.flow-inspector.is-expanded { max-height:90dvh; }
|
||||
.flow-inspector-close,.flow-inspector-expand { display:inline-grid; place-items:center; }
|
||||
.flow-inspector-head { position:sticky; top:-22px; z-index:4; margin:-8px -2px 14px; padding:10px 2px 8px; background:var(--surface); }
|
||||
.flow-inspector-delete { padding-inline:10px; }
|
||||
.flow-inspector .two { grid-template-columns:1fr; }
|
||||
.flow-inspector label { font-size:.78rem; }
|
||||
.flow-inspector input,.flow-inspector select,.flow-inspector textarea { min-height:44px; }
|
||||
|
||||
.flow-block-dialog { max-height:82dvh !important; }
|
||||
.flow-block-library-group > div { grid-template-columns:1fr; }
|
||||
.flow-block-library-group button { min-height:46px; }
|
||||
|
||||
.sensor-alias-row { grid-template-columns:minmax(0,1fr) 34px; }
|
||||
.sensor-alias-entity { grid-column:1 / -1; }
|
||||
.sensor-alias-row input { grid-column:1; }
|
||||
.sensor-alias-clear { grid-column:2; }
|
||||
|
||||
/* Do not let long settings/help copy dominate small screens. */
|
||||
.settings-block { gap:14px; }
|
||||
.settings-block-head h3 { font-size:16px; }
|
||||
.settings-block-head p,.field-note { line-height:1.45; }
|
||||
}
|
||||
|
||||
@media (max-width:420px) {
|
||||
.section-heading h1 { font-size:24px; }
|
||||
.flow-editor-actions .flow-mobile-actions-button { min-width:88px; padding-inline:10px; }
|
||||
}
|
||||
|
||||
/* Readability and form consistency fixes found during the wider UX pass. */
|
||||
textarea { font:inherit; }
|
||||
textarea {
|
||||
width:100%; min-height:96px; resize:vertical; border:1px solid var(--line); border-radius:12px;
|
||||
outline:0; padding:10px 12px; color:var(--text); background:var(--input-bg); line-height:1.45;
|
||||
}
|
||||
textarea:focus { border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 18%,transparent); outline-offset:1px; }
|
||||
.flow-empty-hint button { pointer-events:auto; margin-top:7px; justify-self:center; }
|
||||
#controlPlan .plan-card>p { font-size:11.5px; line-height:1.45; }
|
||||
#controlPlan .plan-events li { font-size:10.5px; line-height:1.4; }
|
||||
#controlPlan .plan-rule-summary strong { font-size:11px; }
|
||||
#controlPlan .plan-rule-summary small { font-size:10px; }
|
||||
#controlPlan .plan-card-head .eyebrow { font-size:9px; }
|
||||
|
||||
@media (prefers-reduced-motion:reduce) {
|
||||
*,*::before,*::after { scroll-behavior:auto !important; animation-duration:.01ms !important; animation-iteration-count:1 !important; transition-duration:.01ms !important; }
|
||||
}
|
||||
.flow-card[data-flow-card-id] { cursor:pointer; }
|
||||
.flow-card[data-flow-card-id]:focus-visible { outline:2px solid color-mix(in srgb,var(--accent) 55%,transparent); outline-offset:2px; }
|
||||
.flow-card[data-flow-card-id] button,.flow-card[data-flow-card-id] details { cursor:pointer; }
|
||||
.flow-generated-card { border-left:3px solid var(--accent-border); }
|
||||
.flow-generated-card .card-footer > small { color:var(--accent); }
|
||||
|
||||
|
||||
/* 0.11.4 production layout guards and in-app chart preview. */
|
||||
html,body { max-width:100%; overflow-x:clip; }
|
||||
main,.view { min-width:0; max-width:100%; }
|
||||
.flow-info-panel,.flow-info-panel > div { min-width:0; }
|
||||
.flow-info-panel > .badge { flex:0 1 auto; max-width:100%; white-space:normal; overflow-wrap:anywhere; text-align:center; }
|
||||
|
||||
body.flow-editor-open { width:100%; height:100vh; height:100dvh; overflow:hidden; overscroll-behavior:none; }
|
||||
.flow-editor { width:100vw; width:100dvw; height:100vh; height:100dvh; max-width:100%; overflow:hidden; }
|
||||
.flow-editor-bar,.flow-editor-title,.flow-editor-actions,.flow-editor-body,.flow-workspace-wrap,.flow-workspace-toolbar { min-width:0; max-width:100%; }
|
||||
|
||||
.chart-title-row > div:first-child { min-width:0; }
|
||||
.chart-title-actions { display:flex; align-items:center; gap:7px; flex:0 0 auto; }
|
||||
.chart-fullscreen-button { min-width:34px; height:32px; padding:0 9px; border:1px solid var(--line); background:var(--surface-muted); color:var(--text-soft); font-size:16px; line-height:1; }
|
||||
.chart-fullscreen-button:hover { background:var(--surface-2); }
|
||||
body.chart-fullscreen-open { overflow:hidden; }
|
||||
body.chart-fullscreen-open::before { content:""; position:fixed; inset:0; z-index:139; background:rgba(0,0,0,.68); backdrop-filter:blur(3px); }
|
||||
.history-chart-card.chart-fullscreen-fallback {
|
||||
position:fixed; left:50%; top:50%; z-index:140;
|
||||
width:min(1200px,calc(100vw - 28px)); height:min(820px,calc(100dvh - 28px)); max-width:none;
|
||||
margin:0; padding:14px; transform:translate(-50%,-50%);
|
||||
display:grid; grid-template-rows:auto minmax(0,1fr) auto; gap:8px; overflow:hidden;
|
||||
border:1px solid var(--line-strong); border-radius:16px; background:var(--surface);
|
||||
box-shadow:0 28px 90px rgba(0,0,0,.58);
|
||||
}
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row { margin:0; }
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row p,
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-hover-hint { display:none; }
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-wrap { height:100%; min-height:0; overflow:auto; border:1px solid var(--line); border-radius:10px; background:var(--surface-muted); }
|
||||
.history-chart-card.chart-fullscreen-fallback .legend { max-height:88px; margin:0; overflow:auto; }
|
||||
|
||||
@media (max-width:760px) {
|
||||
body:not(.flow-editor-open) .bottom-nav { position:fixed; right:0; bottom:0; left:0; z-index:60; max-width:100vw; overflow-x:hidden; }
|
||||
.flow-info-panel { flex-direction:column; align-items:flex-start; gap:12px; padding:16px; overflow:hidden; }
|
||||
.flow-info-panel > div { width:100%; }
|
||||
.flow-info-panel > .badge { align-self:flex-start; text-align:left; }
|
||||
.flow-shared-settings-link { width:100%; max-width:100%; white-space:normal; text-align:left; }
|
||||
.automation-tabs { width:100%; min-width:0; }
|
||||
|
||||
.chart-title-row { gap:8px; }
|
||||
.chart-title-actions { width:100%; justify-content:flex-end; }
|
||||
.chart-fullscreen-button { min-height:36px; height:36px; }
|
||||
.history-chart-card.chart-fullscreen-fallback { width:calc(100vw - 12px); height:calc(100dvh - 18px); padding:9px; border-radius:13px; }
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row { flex-direction:row; align-items:center; gap:8px; }
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row > div:first-child { flex:1 1 auto; min-width:0; }
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row h3 { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-actions { width:auto; flex:0 0 auto; }
|
||||
.history-chart-card.chart-fullscreen-fallback .legend { max-height:70px; }
|
||||
}
|
||||
|
||||
@media (max-width:560px) {
|
||||
.chart-title-actions { gap:5px; }
|
||||
.chart-zoom-controls button { height:36px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user