From 7fd5921ba9b2430ebe6277819ad25e0bd278e309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 4 Sep 2026 08:59:11 +0200 Subject: [PATCH] v0.12.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 32 +- README.md | 2 +- docs/API.md | 6 +- docs/openapi.json | 4 +- .../gree_controller/manifest.json | 4 +- mock/index.html | 34 +- mock/mock.js | 8 + mock/styles.css | 517 ++---------- scripts/api_dev_test.py | 10 +- web/index.html | 34 +- web/js/charts.js | 136 +--- web/js/core.js | 5 - web/js/dashboard.js | 2 +- web/js/entities.js | 3 - web/styles.css | 743 +++--------------- 17 files changed, 276 insertions(+), 1268 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08513df..08b2245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.12.0" +version = "0.12.1" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 26cc0cc..7c80a9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.12.0" +version = "0.12.1" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 34b94ba..899cd21 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,14 +1,14 @@ d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -6877eacd3931f7922f3f379d46e8cd9de7a609a65060ce2285c09b9c95f230aa ./Cargo.lock -ebe64386eb553c298994df40f73a56b4980eedcd88a84cb310134070c1753eb3 ./Cargo.toml +0696b5112a35943786aa4dbe0547b745d158309795a8e8967946eed07cb0d1e7 ./Cargo.lock +000dd8d1a535f357667cf812f817a2abbc72eb79fe062ae2dcf8fef65d6b7491 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -92633f07b839dcdeafa96cb3ffdaaa600f65387a9113ed595987e68982fed0d6 ./README.md +c353091e7695cd19f77bdcad80b41438710a7315c5f2696016dc482000023a21 ./README.md d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs -3596e68d53d614ff784cdaa7a26eabd651cf02909da770bc863518f1218299a0 ./docs/API.md +9596e50efb3ce09afd823d2507c814cfbffe812a622b45c9e0d53526e8cd996a ./docs/API.md 1b522e1b61643733accf0a45b8b862cfb96e1468343c9819600e263fe67a2b96 ./docs/CODE_AUDIT_0.12.0.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md -3ba861e16d523f962574334ed74d53d6be2669dcb05851578d7a5434e8856f4e ./docs/openapi.json +933f560266bd309be0b1eaaa943e86874646e0b9ada5e805bf47a7a62f7063b8 ./docs/openapi.json a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py 6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py @@ -19,7 +19,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 -9538b4a08e6c76dbd5850f3720f582253fd8e4542871891a2cf5552759fb334a ./home-assistant/custom_components/gree_controller/manifest.json +fec5ff50552f331bc0f994d9980059f1b1c309a57419e20f785c04d93dcd14f9 ./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 @@ -31,9 +31,9 @@ c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./home-assista 810bf9a9483277cbde14454b6b88dbfe79b873a6f34737031a7e10d23fda9299 ./lang/en.json ad48d11875aa041c0f593aa6e322f8d953aef137d4df4c369c5f82eec5aebd11 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py -1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html -007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js -c5d515b987d96ebfbc7593e9bac9aff64c8d54420f23cc8df5e2c84b14a69852 ./mock/styles.css +daf25cf3faf39865033e57cc7f79a34b0e0dbca174a37d349611f14d77143e15 ./mock/index.html +efe0fb67500857bfcebb2dcfba77fdc551e5c2e32d2ffff4b092c19528dc679e ./mock/mock.js +6abcb2d3607aa76d763f4ccaa17523f6bc69faaf19e89f0b306559750ff2f066 ./mock/styles.css d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./mock/theme-init.js b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -74,7 +74,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md -966c7b81c07c07d9073f797388f695164095a520753910eb26c5daa64caaae02 ./scripts/api_dev_test.py +cd2eb8c851ab3cacd00efcb5cdbd404f03279e75126c46f025e132ba1e9d4ce1 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -181,13 +181,13 @@ df2f13c98b9d7f84916dcbf9e01a479a6f392f888413fe22bae586526747f704 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -1f3abc20b7373fbb8995fe4331d5b448afb3e8c5a8722b3b0417cb89c684f3de ./web/index.html +d0388bf22f34ae28fb63074b0bc15c087a3be298bb702d006d4814ebb786b36e ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md 8fb20fdba11df68feb0b096bb5b4c8f936db974aed5d345a0d01455054f31f95 ./web/js/bootstrap.js -7dcf8d722407ce0b73dbcfc4982a2a2f3ad5fa82eb9ece227ad850f2117e3d76 ./web/js/charts.js -f75449450a179f5853b8b08c69dec93205985e0dcbc806d355fd6060237761f6 ./web/js/core.js -dfc123885a9b6b5c1d07c12d2d2625ff15aa0844e5cd3736092ccd0a360da39c ./web/js/dashboard.js -563ce41a849574ddfbed281828f34cdcb498bbd04009056c73a378b84bad822a ./web/js/entities.js +badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js +af92b2140f6248bc636d7c75e37a9460f4128ba59adf1b7842d25244a607555c ./web/js/core.js +361d68a9f7dda66a628ebde25d130a26024d809aef521d1b3ad8d8bdf5d39098 ./web/js/dashboard.js +d6fbd8ef4a67269afadb2f59f19fe9a359f7ec4d7bd1473ecff77df117b9558d ./web/js/entities.js e332b3f811958a938105272c1e8e28d6ca8ba69303001da165b4b0b85727695c ./web/js/events.js 1b21b3ae43a6848f9fc13fd4eacaa6a1a50d44c65aa37ca737be60aa4eca8a38 ./web/js/flows.js f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js @@ -199,6 +199,6 @@ c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/route 427376a35832cb3644206c6e89e1654bd807eec78352f80bd0dd43f74b4ec810 ./web/js/settings-ui.js 9dd9fbcc5f00584562bae6a840189495f830d1086d9679763dbad6e9c18c4fdb ./web/js/settings.js 6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest -08e3301706c2ec3ec642dd9c3fc1f8cc1c12ad30113d98fc77ade35bd827d947 ./web/styles.css +9976b7dd54d0075c3bc153f520740d25b7870042c32020b96ac8547669a1f148 ./web/styles.css ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js diff --git a/README.md b/README.md index 6e34b08..56a463d 100644 --- a/README.md +++ b/README.md @@ -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.12.0** +**Current release: 0.12.1** > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. diff --git a/docs/API.md b/docs/API.md index 8e43b15..89d027c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.12.0**. +HTTP and WebSocket API for GREE Controller **0.12.1**. [← Main documentation](../README.md) @@ -230,7 +230,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.12.0", + "version": "0.12.1", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -254,7 +254,7 @@ Returns the initial Web UI snapshot: "house": {"mode": "cool"}, "outdoor_temperature": null, "system": { - "version": "0.12.0", + "version": "0.12.1", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, diff --git a/docs/openapi.json b/docs/openapi.json index 7e46205..6876c7a 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.12.0", + "version": "0.12.1", "summary": "Local HTTP/WebSocket API for GREE HVAC control", "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.", "license": { @@ -4854,7 +4854,7 @@ }, "version": { "type": "string", - "example": "0.12.0" + "example": "0.12.1" }, "uptime_seconds": { "type": "integer", diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index 37cc856..2b01438 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -1,9 +1,9 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.12.0", + "version": "0.12.1", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", "single_config_entry": true -} \ No newline at end of file +} diff --git a/mock/index.html b/mock/index.html index 4e65858..5526068 100644 --- a/mock/index.html +++ b/mock/index.html @@ -270,10 +270,10 @@
-
-
+
+
Live flow -

Automation flow board

+

Automation flow board

Input
-
-
-
+
+
+
Timeline -

What will happen next

+

What will happen next

-
-
-
-
Rules -

Automation rules

+
+
-
-
+ +
+
+
Rules +

Automation rules

+
+
+
+
+
+
diff --git a/mock/mock.js b/mock/mock.js index bba1a33..fa8a656 100644 --- a/mock/mock.js +++ b/mock/mock.js @@ -10,6 +10,14 @@ document.addEventListener('DOMContentLoaded',()=>{ document.querySelector('#houseClimate').innerHTML='

Tryb domu

Wspólne ustawienia dla stref.

Na zewnątrz 12.8°C
'; document.querySelector('#controlPlan').innerHTML='

Salon

Utrzymuj 21.5°C do 22:30

Aktywne

Sypialnia

Tryb nocny od 22:30

za 2h
'; } + if(view==='simulation'){ + document.querySelector('#simulationTarget').innerHTML=''; + document.querySelector('#simulationSummary').innerHTML='
Tryb domuGrzanieStrategia: temperatura zadana
Tryb nocnyWyłączony22:00 → 06:00
Na zewnątrz12.8°CAktualizacja 08:30
Aktywne strefy21 strefa zgłasza zapotrzebowanie
'; + const board=document.querySelector('#simulationFlowBoard'); board.style.width='1325px'; board.style.height='390px'; + board.innerHTML='
SalonGREE Salon
Temperatura

Salon

20.8°C

Home Assistant

Termostat

Salon

21.5°C

Komfort

Decyzja

Grzanie

Potrzeba ciepła

Poniżej celu

Polecenie

GREE Salon

22.0°C · Auto

Quiet: wył.

'; + document.querySelector('#simulationTimeline').innerHTML='
Salon

Komfort · 21.5°C · GREE Salon

Cały dom

Tryb nocny

Sypialnia

Komfort · 20.5°C · GREE Sypialnia

'; + document.querySelector('#simulationRules').innerHTML='
Automatyzacja

Otwarte okno

Stan

Urządzenie: GREE Salon

Wyłącz termostat, gdy okno pozostaje otwarte.
Ostatnio: 07:12Gotowa: teraz
Automatyzacja

Tani prąd

Próg

Urządzenie: GREE Sypialnia

Podnieś temperaturę docelową o 0.5°C.
Ostatnio: wczorajGotowa: teraz
'; + } if(view==='flows'){ document.querySelector('#flowListCount').textContent='3 flow'; document.querySelector('#flowList').innerHTML=`

Okno w salonie

Wyłącza grzanie, gdy okno jest otwarte.

Bloki5
Harmonogramy0
Automatyzacje1

Tani prąd

Podbija temperaturę przy niskiej cenie energii.

Bloki7
Harmonogramy1
Automatyzacje2
`; diff --git a/mock/styles.css b/mock/styles.css index 346d069..bd921f2 100644 --- a/mock/styles.css +++ b/mock/styles.css @@ -176,18 +176,6 @@ button:disabled { white-space: nowrap; } -.toolbar-select { - width: auto; - min-width: 58px; - min-height: 40px; - padding: 7px 28px 7px 10px; - border-radius: 11px; -} - -.theme-select { - min-width: 94px; -} - button { border: 0; border-radius: 13px; @@ -638,16 +626,6 @@ h3 { white-space: nowrap; } -.device-network-info { - display: grid; - min-width: 0; - gap: 2px; -} - -.device-network-error { - color: var(--danger) !important; -} - .card-menu { display: flex; gap: 5px; @@ -867,30 +845,6 @@ h3 { gap: 7px; } -.dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--accent); -} - -.dot.target { - background: var(--warning); -} - -.form-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 14px; -} - -.form-grid h3, -.form-grid hr, -.form-grid .wide { - grid-column: 1/-1; -} - -.form-grid hr, .dialog-form hr { width: 100%; border: 0; @@ -1389,16 +1343,6 @@ legend { font-size: 12px; } - .form-grid { - grid-template-columns: 1fr; - } - - .form-grid h3, - .form-grid hr, - .form-grid .wide { - grid-column: auto; - } - .log-row { grid-template-columns: 62px 1fr; } @@ -1407,9 +1351,6 @@ legend { grid-column: 1/-1; } - .theme-select { - min-width: 86px; - } } @media (max-width: 410px) { @@ -1435,13 +1376,6 @@ legend { grid-template-columns: 1fr; } - .toolbar-select { - min-width: 54px; - } - - .theme-select { - min-width: 82px; - } } .token-manager { @@ -1502,17 +1436,6 @@ legend { } } -/* Zone quick controls */ -.zone-quick-control { - display: grid; - gap: 4px; - margin-top: 10px; - padding: 8px 10px; - border: 1px solid var(--line); - border-radius: 14px; - background: var(--surface-muted); -} - .temperature-control.compact { padding: 4px 0 7px; gap: 16px; @@ -1528,10 +1451,6 @@ legend { font-size: 32px; } -.zone-quick-control .mode-row { - padding-bottom: 2px; -} - .discovery-name-list { display: grid; gap: 12px; @@ -1760,27 +1679,6 @@ legend { background: var(--surface-muted); } -.house-power-panel { - display: flex; - align-items: center; - justify-content: space-between; - gap: 18px; - margin: -15px 0 12px; -} - -.house-power-panel h3 { - margin: 2px 0 4px; - font-size: 18px; -} - -.house-power-panel p { - margin: 0; - max-width: 650px; - color: var(--muted); - font-size: 12px; - line-height: 1.45; -} - .house-power-actions { display: flex; flex: 0 0 auto; @@ -1935,9 +1833,6 @@ legend { } @media (max-width: 620px) { - .house-power-panel { - margin-top: -20px; - } .house-climate-head { align-items: flex-start; @@ -1947,11 +1842,6 @@ legend { max-width: 220px; } - .house-power-panel { - align-items: stretch; - flex-direction: column; - } - .house-power-actions { display: grid; grid-template-columns: 1fr 1fr; @@ -2663,18 +2553,19 @@ legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); gap: 12px; - margin-bottom: 16px; + margin-bottom: 18px; } .simulation-summary-card { display: grid; gap: 6px; - padding: 16px; + padding: 16px 18px; } .simulation-summary-card small { color: var(--muted); - font-size: 12px; + font-size: 11px; + font-weight: 700; } .simulation-summary-card strong { @@ -2688,74 +2579,81 @@ legend { line-height: 1.45; } -.simulation-section-head { +.simulation-section-panel { + margin-bottom: 18px; + padding: 0; + overflow: hidden; +} + +.simulation-panel-head { display: flex; - align-items: end; + align-items: center; justify-content: space-between; - margin: 24px 0 12px; + gap: 16px; + padding: 15px 18px 14px; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--surface-muted) 52%, var(--surface)); } -.simulation-section-head h2 { - margin: 4px 0 0; +.simulation-panel-head > div:first-child { + min-width: 0; } -.simulation-timeline-panel { - padding: 16px; - margin-bottom: 22px; +.simulation-panel-head .eyebrow { + font-size: 9px; +} + +.simulation-panel-head h2 { + margin: 3px 0 0; + font-size: 19px; + letter-spacing: -.025em; +} + +.simulation-panel-body { + padding: 16px 18px 18px; } .simulation-timeline { display: grid; - gap: 10px; } .simulation-timeline-item { display: grid; - grid-template-columns: 110px 1fr; + grid-template-columns: 110px minmax(0, 1fr); gap: 12px; align-items: start; padding: 12px 0; border-bottom: 1px solid var(--line); } +.simulation-timeline-item:first-child { + padding-top: 0; +} + .simulation-timeline-item:last-child { - border-bottom: 0; padding-bottom: 0; + border-bottom: 0; } .simulation-timeline-item time { color: var(--muted); font-size: 12px; + font-variant-numeric: tabular-nums; } .simulation-timeline-item strong { display: block; margin-bottom: 4px; + font-size: 14px; } .simulation-timeline-item p { margin: 0; color: var(--muted); + font-size: 12px; line-height: 1.45; } -.simulation-zone-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); - gap: 14px; -} - -.simulation-zone-card { - display: grid; - gap: 14px; - padding: 16px; -} - -.simulation-zone-card.disabled { - opacity: .68; -} - -.simulation-zone-head, .simulation-rule-head { display: flex; align-items: flex-start; @@ -2763,140 +2661,62 @@ legend { gap: 10px; } -.simulation-zone-head h3, .simulation-rule-head h3 { margin: 3px 0 0; + font-size: 16px; } -.simulation-zone-head p, -.simulation-rule-head p { - margin: 6px 0 0; - color: var(--muted); - font-size: 13px; - line-height: 1.45; -} - -.simulation-flow { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.simulation-step { - display: grid; - gap: 6px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 16px; - background: rgba(255, 255, 255, .02); - min-height: 112px; -} - -html[data-theme='light'] .simulation-step { - background: rgba(0, 0, 0, .02); -} - -.simulation-step.accent { - box-shadow: inset 0 0 0 1px rgba(101, 163, 255, .28); -} - -.simulation-step small, -.simulation-metrics small, .simulation-rule-meta small { color: var(--muted); font-size: 11px; } -.simulation-step strong { - font-size: 15px; - line-height: 1.35; -} - -.simulation-step span { - color: var(--muted); - font-size: 12px; - line-height: 1.4; -} - -.simulation-metrics { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.simulation-metrics>div, -.simulation-rule-action { - padding: 12px; - border-radius: 14px; - border: 1px solid var(--line); - background: rgba(255, 255, 255, .02); -} - -html[data-theme='light'] .simulation-metrics>div, -html[data-theme='light'] .simulation-rule-action { - background: rgba(0, 0, 0, .02); -} - -.simulation-metrics strong { - display: block; - margin-top: 4px; -} - -.simulation-events { - padding-top: 0; - border-top: 0; -} - .simulation-rules-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); - gap: 14px; + gap: 12px; } .simulation-rule-card { display: grid; gap: 12px; - padding: 16px; + min-width: 0; + padding: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--surface-muted); } -.simulation-rule-card>p { +.simulation-rule-card > p { margin: 0; color: var(--muted); + font-size: 12px; + line-height: 1.45; } .simulation-rule-action { - font-size: 14px; + padding: 11px 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface); + font-size: 13px; line-height: 1.5; } .simulation-rule-meta { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 6px 12px; color: var(--muted); } -@media (max-width: 900px) { - - .simulation-flow, - .simulation-metrics { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - @media (max-width: 640px) { .simulation-timeline-item { grid-template-columns: 1fr; - gap: 6px; - } - - .simulation-flow, - .simulation-metrics { - grid-template-columns: 1fr; + gap: 5px; } } -/* Compact connection state: a colored dot replaces the Connected/Disconnected label. */ .brand-line { display: flex; align-items: center; @@ -3050,25 +2870,6 @@ html[data-theme='light'] .simulation-rule-action { grid-column: 1/-1; } -.settings-subhead { - display: flex; - justify-content: space-between; - gap: 12px; - margin-bottom: 10px; -} - -.settings-subhead strong, -.settings-subhead small { - display: block; -} - -.settings-subhead small { - margin-top: 4px; - color: var(--muted); - font-size: 11px; - line-height: 1.4; -} - .settings-save-bar { position: sticky; bottom: 18px; @@ -3157,25 +2958,6 @@ html[data-theme='light'] .simulation-rule-action { line-height: 1.4; } -.simulation-flow-shell { - padding: 0; - overflow: hidden; - margin-bottom: 18px; -} - -.simulation-flow-head { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 16px; - padding: 18px 18px 14px; - border-bottom: 1px solid var(--line); -} - -.simulation-flow-head h2 { - margin: 3px 0 0; -} - .flow-legend { display: flex; flex-wrap: wrap; @@ -3465,7 +3247,7 @@ html[data-theme='light'] .simulation-rule-action { align-items: stretch; } - .simulation-flow-head { + .simulation-panel-head { align-items: flex-start; flex-direction: column; } @@ -3642,29 +3424,6 @@ html[data-theme='light'] .simulation-rule-action { display: none !important; } -.dashboard-group-panel { - display: grid; - gap: 14px; - margin-bottom: 18px; -} - -.dashboard-group-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.dashboard-group-head h3 { - margin: 3px 0 5px; -} - -.dashboard-group-head p { - margin: 0; - color: var(--muted); - line-height: 1.45; -} - .group-quick-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr)); @@ -3859,7 +3618,6 @@ body.simulation-standalone [data-view="simulation"] { @media (max-width:720px) { - .dashboard-group-head, .simulation-page-head { align-items: stretch; flex-direction: column; @@ -4105,84 +3863,6 @@ body.simulation-standalone [data-view="simulation"] { line-height: 1.15; } -.dashboard-secondary-stack { - display: grid; - gap: 8px; - margin-top: 4px; -} - -.dashboard-disclosure { - overflow: hidden; - padding: 0; -} - -.dashboard-disclosure>summary { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 13px 15px; - cursor: pointer; - list-style: none; - user-select: none; -} - -.dashboard-disclosure>summary::-webkit-details-marker { - display: none; -} - -.dashboard-disclosure>summary h3 { - margin: 2px 0 0; - font-size: 16px; -} - -.dashboard-disclosure>summary .eyebrow { - font-size: 9px; -} - -.dashboard-summary-side { - display: flex; - align-items: center; - gap: 10px; -} - -.dashboard-summary-side .badge { - min-width: 28px; - justify-content: center; -} - -.dashboard-disclosure-chevron { - display: inline-block; - color: var(--muted); - font-size: 18px; - line-height: 1; - transition: transform .18s ease; -} - -.dashboard-disclosure[open]>summary { - border-bottom: 1px solid var(--line); -} - -.dashboard-disclosure[open] .dashboard-disclosure-chevron { - transform: rotate(180deg); -} - -.dashboard-disclosure-body { - padding: 12px; -} - -.dashboard-disclosure-body .automation-plan-grid, -.dashboard-disclosure-body .dashboard-zones { - margin-bottom: 0; -} - -.dashboard-disclosure-body .device-grid, -.dashboard-disclosure-body .list-grid, -.dashboard-disclosure-body .automation-plan-grid { - gap: 10px; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); -} - /* Dashboard quick controls share one compact visual language. */ .quick-control-card { --quick-control-height: 28px; @@ -4197,10 +3877,6 @@ body.simulation-standalone [data-view="simulation"] { padding: 2px 0 5px; } -.quick-control-card .quick-control-row-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - .quick-control-card .quick-control-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -4449,14 +4125,6 @@ body.simulation-standalone [data-view="simulation"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .dashboard-disclosure>summary { - padding: 12px 13px; - } - - .dashboard-disclosure-body { - padding: 10px; - } - .quick-thermostat-control .list-card-head { align-items: center; } @@ -5157,57 +4825,6 @@ body.simulation-standalone [data-view="simulation"] { min-width: 0; } -.language-picker { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 5px; - width: 58px; - height: 40px; - border: 1px solid var(--line); - border-radius: 11px; - background: var(--surface-muted); - color: var(--text-soft); - transition: border-color .15s ease, background .15s ease, transform .15s ease; -} - -.language-picker:hover { - background: var(--surface-2); -} - -.language-picker:active { - transform: scale(.97); -} - -.language-label { - font-size: 11px; - font-weight: 850; - letter-spacing: .06em; - line-height: 1; -} - -.language-chevron { - margin-top: -2px; - color: var(--muted); - font-size: 13px; - line-height: 1; -} - -.language-select { - position: absolute; - inset: 0; - width: 100%; - min-width: 0; - height: 100%; - min-height: 0; - margin: 0; - padding: 0; - border: 0; - opacity: 0; - cursor: pointer; -} - .toolbar-icon-control { display: inline-grid; place-items: center; @@ -5232,8 +4849,7 @@ body.simulation-standalone [data-view="simulation"] { font-size: 21px; } -button:focus-visible, -.language-picker:focus-within { +button:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: 2px; } @@ -5338,12 +4954,6 @@ button:focus-visible, gap: 5px; } - .language-picker { - width: 50px; - height: 38px; - border-radius: 10px; - } - .toolbar-icon-control { flex-basis: 38px; width: 38px; @@ -5352,9 +4962,6 @@ button:focus-visible, border-radius: 10px; } - .language-chevron { - font-size: 11px; - } } @media (max-width: 360px) { @@ -6004,7 +5611,6 @@ textarea[aria-invalid="true"] { flex: 0 0 auto; } - /* v0.9.1: global compressor queue overview. */ .queue-overview-button { display: inline-flex; @@ -6450,7 +6056,6 @@ body.flow-editor-open { overflow:hidden; } .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); } @@ -6730,8 +6335,7 @@ textarea:focus { border-color:var(--accent); outline:2px solid color-mix(in srgb .flow-generated-card { border-left:3px solid var(--accent-border); } .flow-generated-card .card-footer > small { color:var(--accent); } - -/* 0.12.0 production layout guards and in-app chart preview. */ +/* 0.12.x 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; } @@ -7112,8 +6716,7 @@ body.chart-fullscreen-open::before { content:""; position:fixed; inset:0; z-inde .ping-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } - -/* 0.12.0 diagnostics, manual-control safety and technical connection checks. */ +/* 0.12.x diagnostics, manual-control safety and technical connection checks. */ .manual-device-problem { display: grid; gap: 4px; diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index 24d5f40..23b37d5 100644 --- a/scripts/api_dev_test.py +++ b/scripts/api_dev_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Developer smoke/integration tests for GREE Controller API 0.12.0. +"""Developer smoke/integration tests for GREE Controller API 0.12.1. Default mode is read-only and safe to run against a real controller. Use --settings-write to additionally round-trip all split settings resources and @@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787") DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "") -DEFAULT_EXPECTED_VERSION = "0.12.0" +DEFAULT_EXPECTED_VERSION = "0.12.1" SETTINGS_PATHS: dict[str, set[str]] = { "/api/settings/application": {"simulator_enabled"}, @@ -331,7 +331,7 @@ def test_openapi(client: ApiClient, expected_version: str) -> str: raise TestFailure("OpenAPI has no paths object") missing = sorted(EXPECTED_OPENAPI_PATHS - set(paths)) if missing: - raise TestFailure(f"OpenAPI missing 0.12.0 paths: {missing}") + raise TestFailure(f"OpenAPI missing required paths: {missing}") forbidden = sorted(set(REMOVED_0120_PATHS) & set(paths)) if forbidden: raise TestFailure(f"OpenAPI still exposes removed paths: {forbidden}") @@ -503,7 +503,7 @@ def run_suite(args: argparse.Namespace) -> int: print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n") runner.run("public health", lambda: test_health(client, args.expected_version)) - runner.run("OpenAPI 0.12.0 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("OpenAPI 0.12.1 contract", lambda: test_openapi(client, args.expected_version)) runner.run("protected API requires auth", lambda: test_protected_auth(client)) for path in SAFE_GET_PATHS: @@ -571,7 +571,7 @@ def run_suite(args: argparse.Namespace) -> int: def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Developer smoke/integration tests for GREE Controller API 0.12.0.", + description="Developer smoke/integration tests for GREE Controller API 0.12.1.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""Examples: python3 scripts/api_dev_test.py diff --git a/web/index.html b/web/index.html index 4828bea..192ffe8 100644 --- a/web/index.html +++ b/web/index.html @@ -280,10 +280,10 @@
-
-
+
+
Live flow -

Automation flow board

+

Automation flow board

Input
-
-
-
+
+
+
Timeline -

What will happen next

+

What will happen next

-
-
-
-
Rules -

Automation rules

+
+
-
-
+ +
+
+
Rules +

Automation rules

+
+
+
+
+
+
diff --git a/web/js/charts.js b/web/js/charts.js index 86cab41..493b73a 100644 --- a/web/js/charts.js +++ b/web/js/charts.js @@ -151,6 +151,9 @@ function updateChartFullscreenButton(card) { function closeChartPreview(card) { if (!card) return; card.classList.remove('chart-fullscreen-fallback'); + card.removeAttribute('role'); + card.removeAttribute('aria-modal'); + card.removeAttribute('aria-label'); document.body.classList.remove('chart-fullscreen-open'); updateChartFullscreenButton(card); const canvas = card.querySelector('canvas[id]'); @@ -158,6 +161,7 @@ function closeChartPreview(card) { } function toggleChartFullscreen(id) { + if (!window.matchMedia('(min-width: 761px)').matches) return; const canvas = document.getElementById(id); const card = canvas?.closest('.history-chart-card'); if (!card) return; @@ -172,12 +176,10 @@ function toggleChartFullscreen(id) { } card.classList.add('chart-fullscreen-fallback'); + card.setAttribute('role', 'dialog'); + card.setAttribute('aria-modal', 'true'); + card.setAttribute('aria-label', card.querySelector('h3')?.textContent || tr('history.fullscreen')); document.body.classList.add('chart-fullscreen-open'); - if (window.matchMedia('(max-width: 760px)').matches) { - app.chartZooms[id] = 1; - canvas.dataset.chartZoom = '1'; - if (canvas.parentElement) canvas.parentElement.scrollLeft = 0; - } updateChartFullscreenButton(card); requestAnimationFrame(() => redrawHistoryChart(id)); } @@ -186,15 +188,12 @@ function prepareCanvas(canvas, height) { const rect = canvas.getBoundingClientRect(); const wrap = canvas.parentElement; const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720); - const card = canvas.closest('.history-chart-card'); - const fullscreen = chartCardIsFullscreen(card); - const mobileFullscreen = fullscreen && window.matchMedia('(max-width: 760px)').matches; + const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card')); const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0; - const naturalMobileHeight = Math.round(clamp(wrapWidth * .72, 260, 360)); - const actualHeight = mobileFullscreen ? Math.min(wrapHeight || naturalMobileHeight, naturalMobileHeight) : (wrapHeight || height); + const actualHeight = wrapHeight || height; const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM); - // Normal charts may scroll horizontally. Fullscreen 100% always fits the viewport width. - const baseWidth = fullscreen ? Math.max(1, wrapWidth) : Math.max(720, wrapWidth); + // Keep the original chart geometry on narrow screens and let the wrapper scroll horizontally. + const baseWidth = Math.max(720, wrapWidth); const width = Math.max(1, Math.floor(baseWidth * zoom)); const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = actualHeight * dpr; @@ -262,10 +261,7 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) { .filter(point => Number.isFinite(point.ts) && Number.isFinite(point.value)) .sort((a, b) => a.ts - b.ts) })); - const touches = new Map(); let drag = null; - let pinch = null; - let touchPan = null; const hide = () => { tooltip.hidden = true; line.hidden = true; }; const hideSelection = () => { if (selection) selection.hidden = true; }; @@ -323,114 +319,42 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) { }; canvas.onpointerdown = event => { - if (event.pointerType === 'mouse') { - if (event.button !== 0) return; - const startX = canvasX(event.clientX); - drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false }; - hide(); hideSelection(); - canvas.setPointerCapture?.(event.pointerId); - event.preventDefault(); - return; - } - if (event.pointerType === 'touch') { - touches.set(event.pointerId, { x: event.clientX, y: event.clientY }); - if (touches.size === 1) { - touchPan = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, scrollLeft: wrap.scrollLeft, moved: false }; - showFromClientX(event.clientX); - } - if (touches.size === 2) { - touchPan = null; - const points = [...touches.values()]; - const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y); - const midX = (points[0].x + points[1].x) / 2; - const rect = canvas.getBoundingClientRect(); - const wrapRect = wrap.getBoundingClientRect(); - pinch = { - startDistance: Math.max(1, distance), - startZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM), - baseWidth: (rect.width || width) / clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM), - anchorRatio: clamp((midX - rect.left) / Math.max(1, rect.width), 0, 1), - viewportX: clamp(midX - wrapRect.left, 0, wrap.clientWidth), - previewZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM), - }; - hide(); - event.preventDefault(); - } - } + if (event.pointerType !== 'mouse' || event.button !== 0) return; + const startX = canvasX(event.clientX); + drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false }; + hide(); hideSelection(); + canvas.setPointerCapture?.(event.pointerId); + event.preventDefault(); }; canvas.onpointermove = event => { + if (event.pointerType === 'touch') return; if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) { updateSelection(canvasX(event.clientX)); event.preventDefault(); return; } - if (event.pointerType === 'touch' && touches.has(event.pointerId)) { - touches.set(event.pointerId, { x: event.clientX, y: event.clientY }); - if (pinch && touches.size >= 2) { - const points = [...touches.values()].slice(0, 2); - const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y); - pinch.previewZoom = clamp(pinch.startZoom * (distance / pinch.startDistance), 1, MAX_CHART_ZOOM); - const previewWidth = pinch.baseWidth * pinch.previewZoom; - canvas.style.width = `${previewWidth}px`; - wrap.scrollLeft = Math.max(0, pinch.anchorRatio * previewWidth - pinch.viewportX); - const reset = canvas.closest('.history-chart-card')?.querySelector('[data-chart-zoom="reset"]'); - if (reset) reset.textContent = `${Math.round(pinch.previewZoom * 100)}%`; - event.preventDefault(); - } else if (touchPan?.pointerId === event.pointerId && touches.size === 1) { - const dx = event.clientX - touchPan.startX; - const dy = event.clientY - touchPan.startY; - if (Math.abs(dx) > 6 && Math.abs(dx) > Math.abs(dy)) { - touchPan.moved = true; - hide(); - wrap.scrollLeft = Math.max(0, touchPan.scrollLeft - dx); - event.preventDefault(); - } - } - return; - } showFromClientX(event.clientX); }; const finishPointer = event => { - if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) { - const currentX = canvasX(event.clientX); - const selectedWidth = Math.abs(currentX - drag.startX); - const center = (currentX + drag.startX) / 2; - const moved = drag.moved && selectedWidth >= 12; - drag = null; hideSelection(); - try { canvas.releasePointerCapture?.(event.pointerId); } catch (_) { } - if (moved) { - const currentZoom = clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM); - const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth)); - setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2); - } else showFromClientX(event.clientX); - return; - } - if (event.pointerType === 'touch') { - const wasPan = touchPan?.pointerId === event.pointerId ? touchPan : null; - touches.delete(event.pointerId); - if (pinch && touches.size < 2) { - const completed = pinch; - pinch = null; - setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX); - } else if (wasPan && !wasPan.moved) showFromClientX(event.clientX); - if (touches.size === 0) touchPan = null; - } + if (event.pointerType !== 'mouse' || drag?.pointerId !== event.pointerId) return; + const currentX = canvasX(event.clientX); + const selectedWidth = Math.abs(currentX - drag.startX); + const center = (currentX + drag.startX) / 2; + const moved = drag.moved && selectedWidth >= 12; + drag = null; hideSelection(); + try { canvas.releasePointerCapture?.(event.pointerId); } catch (_) { } + if (moved) { + const currentZoom = clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM); + const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth)); + setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2); + } else showFromClientX(event.clientX); }; canvas.onpointerup = finishPointer; canvas.onpointercancel = event => { if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) { drag = null; hideSelection(); } - if (event.pointerType === 'touch') { - touches.delete(event.pointerId); - if (touchPan?.pointerId === event.pointerId) touchPan = null; - if (pinch && touches.size < 2) { - const completed = pinch; - pinch = null; - setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX); - } else hide(); - } }; canvas.onpointerleave = event => { if (event.pointerType !== 'touch' && !drag) hide(); }; canvas.onkeydown = event => { diff --git a/web/js/core.js b/web/js/core.js index 1e1e683..5d8b1c8 100644 --- a/web/js/core.js +++ b/web/js/core.js @@ -307,11 +307,6 @@ function setTheme(theme) { applyTheme(); } -function cycleTheme() { - const modes = ['system', 'light', 'dark']; - const index = modes.indexOf(app.theme); - setTheme(modes[(index + 1) % modes.length]); -} async function api(path, options = {}) { const headers = new Headers(options.headers || {}); diff --git a/web/js/dashboard.js b/web/js/dashboard.js index d3dc062..538c919 100644 --- a/web/js/dashboard.js +++ b/web/js/dashboard.js @@ -406,7 +406,7 @@ function renderSimulationPage() { }).join('') : `
${esc(tr('plan.noEvents'))}
`; const activeRules = (plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones)); - rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `
${esc(tr('common.automation'))}

${esc(rule.name)}

${esc(automationTriggerLabel(rule))}

${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}

${esc(simulationRuleAction(rule))}
${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}
`).join('') : `
${esc(tr('simulation.noRules'))}
`; + rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `
${esc(tr('common.automation'))}

${esc(rule.name)}

${esc(automationTriggerLabel(rule))}

${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}

${esc(simulationRuleAction(rule))}
${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}
`).join('') : `
${esc(tr('simulation.noRules'))}
`; } function scheduleControlPlanLoad() { diff --git a/web/js/entities.js b/web/js/entities.js index fd60f33..071093b 100644 --- a/web/js/entities.js +++ b/web/js/entities.js @@ -10,9 +10,6 @@ function deviceFeaturePanel(device) { return `
${esc(tr('devices.features'))}
${features.map(([field, , label]) => ``).join('')}
`; } -function deviceZoneDisabled(deviceId) { - return app.zones.some(zone => zone.device_id === deviceId && zone.enabled === false); -} function zoneForDevice(deviceId) { return app.zones.find(zone => zone.device_id === deviceId) || null; diff --git a/web/styles.css b/web/styles.css index 18f1cb6..6572ff6 100644 --- a/web/styles.css +++ b/web/styles.css @@ -178,18 +178,6 @@ button:disabled { white-space: nowrap; } -.toolbar-select { - width: auto; - min-width: 58px; - min-height: 40px; - padding: 7px 28px 7px 10px; - border-radius: 11px; -} - -.theme-select { - min-width: 94px; -} - button { border: 0; border-radius: 13px; @@ -640,16 +628,6 @@ h3 { white-space: nowrap; } -.device-network-info { - display: grid; - min-width: 0; - gap: 2px; -} - -.device-network-error { - color: var(--danger) !important; -} - .card-menu { display: flex; gap: 5px; @@ -869,30 +847,6 @@ h3 { gap: 7px; } -.dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--accent); -} - -.dot.target { - background: var(--warning); -} - -.form-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 14px; -} - -.form-grid h3, -.form-grid hr, -.form-grid .wide { - grid-column: 1/-1; -} - -.form-grid hr, .dialog-form hr { width: 100%; border: 0; @@ -1391,16 +1345,6 @@ legend { font-size: 12px; } - .form-grid { - grid-template-columns: 1fr; - } - - .form-grid h3, - .form-grid hr, - .form-grid .wide { - grid-column: auto; - } - .log-row { grid-template-columns: 62px 1fr; } @@ -1409,9 +1353,6 @@ legend { grid-column: 1/-1; } - .theme-select { - min-width: 86px; - } } @media (max-width: 410px) { @@ -1437,13 +1378,6 @@ legend { grid-template-columns: 1fr; } - .toolbar-select { - min-width: 54px; - } - - .theme-select { - min-width: 82px; - } } .token-manager { @@ -1504,16 +1438,6 @@ legend { } } -.zone-quick-control { - display: grid; - gap: 4px; - margin-top: 10px; - padding: 8px 10px; - border: 1px solid var(--line); - border-radius: 14px; - background: var(--surface-muted); -} - .temperature-control.compact { padding: 4px 0 7px; gap: 16px; @@ -1529,10 +1453,6 @@ legend { font-size: 32px; } -.zone-quick-control .mode-row { - padding-bottom: 2px; -} - .discovery-name-list { display: grid; gap: 12px; @@ -1760,27 +1680,6 @@ legend { background: var(--surface-muted); } -.house-power-panel { - display: flex; - align-items: center; - justify-content: space-between; - gap: 18px; - margin: -15px 0 12px; -} - -.house-power-panel h3 { - margin: 2px 0 4px; - font-size: 18px; -} - -.house-power-panel p { - margin: 0; - max-width: 650px; - color: var(--muted); - font-size: 12px; - line-height: 1.45; -} - .house-power-actions { display: flex; flex: 0 0 auto; @@ -1935,9 +1834,6 @@ legend { } @media (max-width: 620px) { - .house-power-panel { - margin-top: -20px; - } .house-climate-head { align-items: flex-start; @@ -1947,11 +1843,6 @@ legend { max-width: 220px; } - .house-power-panel { - align-items: stretch; - flex-direction: column; - } - .house-power-actions { display: grid; grid-template-columns: 1fr 1fr; @@ -2660,18 +2551,19 @@ legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); gap: 12px; - margin-bottom: 16px; + margin-bottom: 18px; } .simulation-summary-card { display: grid; gap: 6px; - padding: 16px; + padding: 16px 18px; } .simulation-summary-card small { color: var(--muted); - font-size: 12px; + font-size: 11px; + font-weight: 700; } .simulation-summary-card strong { @@ -2685,74 +2577,81 @@ legend { line-height: 1.45; } -.simulation-section-head { +.simulation-section-panel { + margin-bottom: 18px; + padding: 0; + overflow: hidden; +} + +.simulation-panel-head { display: flex; - align-items: end; + align-items: center; justify-content: space-between; - margin: 24px 0 12px; + gap: 16px; + padding: 15px 18px 14px; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--surface-muted) 52%, var(--surface)); } -.simulation-section-head h2 { - margin: 4px 0 0; +.simulation-panel-head > div:first-child { + min-width: 0; } -.simulation-timeline-panel { - padding: 16px; - margin-bottom: 22px; +.simulation-panel-head .eyebrow { + font-size: 9px; +} + +.simulation-panel-head h2 { + margin: 3px 0 0; + font-size: 19px; + letter-spacing: -.025em; +} + +.simulation-panel-body { + padding: 16px 18px 18px; } .simulation-timeline { display: grid; - gap: 10px; } .simulation-timeline-item { display: grid; - grid-template-columns: 110px 1fr; + grid-template-columns: 110px minmax(0, 1fr); gap: 12px; align-items: start; padding: 12px 0; border-bottom: 1px solid var(--line); } +.simulation-timeline-item:first-child { + padding-top: 0; +} + .simulation-timeline-item:last-child { - border-bottom: 0; padding-bottom: 0; + border-bottom: 0; } .simulation-timeline-item time { color: var(--muted); font-size: 12px; + font-variant-numeric: tabular-nums; } .simulation-timeline-item strong { display: block; margin-bottom: 4px; + font-size: 14px; } .simulation-timeline-item p { margin: 0; color: var(--muted); + font-size: 12px; line-height: 1.45; } -.simulation-zone-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); - gap: 14px; -} - -.simulation-zone-card { - display: grid; - gap: 14px; - padding: 16px; -} - -.simulation-zone-card.disabled { - opacity: .68; -} - -.simulation-zone-head, .simulation-rule-head { display: flex; align-items: flex-start; @@ -2760,136 +2659,59 @@ legend { gap: 10px; } -.simulation-zone-head h3, .simulation-rule-head h3 { margin: 3px 0 0; + font-size: 16px; } -.simulation-zone-head p, -.simulation-rule-head p { - margin: 6px 0 0; - color: var(--muted); - font-size: 13px; - line-height: 1.45; -} - -.simulation-flow { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.simulation-step { - display: grid; - gap: 6px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 16px; - background: rgba(255, 255, 255, .02); - min-height: 112px; -} - -html[data-theme='light'] .simulation-step { - background: rgba(0, 0, 0, .02); -} - -.simulation-step.accent { - box-shadow: inset 0 0 0 1px rgba(101, 163, 255, .28); -} - -.simulation-step small, -.simulation-metrics small, .simulation-rule-meta small { color: var(--muted); font-size: 11px; } -.simulation-step strong { - font-size: 15px; - line-height: 1.35; -} - -.simulation-step span { - color: var(--muted); - font-size: 12px; - line-height: 1.4; -} - -.simulation-metrics { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.simulation-metrics>div, -.simulation-rule-action { - padding: 12px; - border-radius: 14px; - border: 1px solid var(--line); - background: rgba(255, 255, 255, .02); -} - -html[data-theme='light'] .simulation-metrics>div, -html[data-theme='light'] .simulation-rule-action { - background: rgba(0, 0, 0, .02); -} - -.simulation-metrics strong { - display: block; - margin-top: 4px; -} - -.simulation-events { - padding-top: 0; - border-top: 0; -} - .simulation-rules-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); - gap: 14px; + gap: 12px; } .simulation-rule-card { display: grid; gap: 12px; - padding: 16px; + min-width: 0; + padding: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--surface-muted); } -.simulation-rule-card>p { +.simulation-rule-card > p { margin: 0; color: var(--muted); + font-size: 12px; + line-height: 1.45; } .simulation-rule-action { - font-size: 14px; + padding: 11px 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface); + font-size: 13px; line-height: 1.5; } .simulation-rule-meta { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 6px 12px; color: var(--muted); } -@media (max-width: 900px) { - - .simulation-flow, - .simulation-metrics { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - @media (max-width: 640px) { .simulation-timeline-item { grid-template-columns: 1fr; - gap: 6px; - } - - .simulation-flow, - .simulation-metrics { - grid-template-columns: 1fr; + gap: 5px; } } @@ -3045,25 +2867,6 @@ html[data-theme='light'] .simulation-rule-action { grid-column: 1/-1; } -.settings-subhead { - display: flex; - justify-content: space-between; - gap: 12px; - margin-bottom: 10px; -} - -.settings-subhead strong, -.settings-subhead small { - display: block; -} - -.settings-subhead small { - margin-top: 4px; - color: var(--muted); - font-size: 11px; - line-height: 1.4; -} - .settings-save-bar { position: sticky; bottom: 18px; @@ -3152,25 +2955,6 @@ html[data-theme='light'] .simulation-rule-action { line-height: 1.4; } -.simulation-flow-shell { - padding: 0; - overflow: hidden; - margin-bottom: 18px; -} - -.simulation-flow-head { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 16px; - padding: 18px 18px 14px; - border-bottom: 1px solid var(--line); -} - -.simulation-flow-head h2 { - margin: 3px 0 0; -} - .flow-legend { display: flex; flex-wrap: wrap; @@ -3460,7 +3244,7 @@ html[data-theme='light'] .simulation-rule-action { align-items: stretch; } - .simulation-flow-head { + .simulation-panel-head { align-items: flex-start; flex-direction: column; } @@ -3634,29 +3418,6 @@ html[data-theme='light'] .simulation-rule-action { display: none !important; } -.dashboard-group-panel { - display: grid; - gap: 14px; - margin-bottom: 18px; -} - -.dashboard-group-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.dashboard-group-head h3 { - margin: 3px 0 5px; -} - -.dashboard-group-head p { - margin: 0; - color: var(--muted); - line-height: 1.45; -} - .group-quick-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr)); @@ -3851,7 +3612,6 @@ body.simulation-standalone [data-view="simulation"] { @media (max-width:720px) { - .dashboard-group-head, .simulation-page-head { align-items: stretch; flex-direction: column; @@ -4095,84 +3855,6 @@ body.simulation-standalone [data-view="simulation"] { line-height: 1.15; } -.dashboard-secondary-stack { - display: grid; - gap: 8px; - margin-top: 4px; -} - -.dashboard-disclosure { - overflow: hidden; - padding: 0; -} - -.dashboard-disclosure>summary { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 13px 15px; - cursor: pointer; - list-style: none; - user-select: none; -} - -.dashboard-disclosure>summary::-webkit-details-marker { - display: none; -} - -.dashboard-disclosure>summary h3 { - margin: 2px 0 0; - font-size: 16px; -} - -.dashboard-disclosure>summary .eyebrow { - font-size: 9px; -} - -.dashboard-summary-side { - display: flex; - align-items: center; - gap: 10px; -} - -.dashboard-summary-side .badge { - min-width: 28px; - justify-content: center; -} - -.dashboard-disclosure-chevron { - display: inline-block; - color: var(--muted); - font-size: 18px; - line-height: 1; - transition: transform .18s ease; -} - -.dashboard-disclosure[open]>summary { - border-bottom: 1px solid var(--line); -} - -.dashboard-disclosure[open] .dashboard-disclosure-chevron { - transform: rotate(180deg); -} - -.dashboard-disclosure-body { - padding: 12px; -} - -.dashboard-disclosure-body .automation-plan-grid, -.dashboard-disclosure-body .dashboard-zones { - margin-bottom: 0; -} - -.dashboard-disclosure-body .device-grid, -.dashboard-disclosure-body .list-grid, -.dashboard-disclosure-body .automation-plan-grid { - gap: 10px; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); -} - .quick-control-card { --quick-control-height: 28px; --quick-control-gap: 4px; @@ -4186,10 +3868,6 @@ body.simulation-standalone [data-view="simulation"] { padding: 2px 0 5px; } -.quick-control-card .quick-control-row-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - .quick-control-card .quick-control-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -4436,14 +4114,6 @@ body.simulation-standalone [data-view="simulation"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .dashboard-disclosure>summary { - padding: 12px 13px; - } - - .dashboard-disclosure-body { - padding: 10px; - } - .quick-thermostat-control .list-card-head { align-items: center; } @@ -5133,57 +4803,6 @@ body.simulation-standalone [data-view="simulation"] { min-width: 0; } -.language-picker { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 5px; - width: 58px; - height: 40px; - border: 1px solid var(--line); - border-radius: 11px; - background: var(--surface-muted); - color: var(--text-soft); - transition: border-color .15s ease, background .15s ease, transform .15s ease; -} - -.language-picker:hover { - background: var(--surface-2); -} - -.language-picker:active { - transform: scale(.97); -} - -.language-label { - font-size: 11px; - font-weight: 850; - letter-spacing: .06em; - line-height: 1; -} - -.language-chevron { - margin-top: -2px; - color: var(--muted); - font-size: 13px; - line-height: 1; -} - -.language-select { - position: absolute; - inset: 0; - width: 100%; - min-width: 0; - height: 100%; - min-height: 0; - margin: 0; - padding: 0; - border: 0; - opacity: 0; - cursor: pointer; -} - .toolbar-icon-control { display: inline-grid; place-items: center; @@ -5208,8 +4827,7 @@ body.simulation-standalone [data-view="simulation"] { font-size: 21px; } -button:focus-visible, -.language-picker:focus-within { +button:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: 2px; } @@ -5314,12 +4932,6 @@ button:focus-visible, gap: 5px; } - .language-picker { - width: 50px; - height: 38px; - border-radius: 10px; - } - .toolbar-icon-control { flex-basis: 38px; width: 38px; @@ -5328,9 +4940,6 @@ button:focus-visible, border-radius: 10px; } - .language-chevron { - font-size: 11px; - } } @media (max-width: 360px) { @@ -5653,13 +5262,8 @@ textarea[aria-invalid="true"] { font-variant-numeric: tabular-nums; } -.chart-wrap { - overscroll-behavior-inline: contain; - scroll-behavior: smooth; -} - .chart-wrap canvas { - touch-action: pan-y; + touch-action: auto; user-select: none; -webkit-user-select: none; } @@ -5710,14 +5314,6 @@ textarea[aria-invalid="true"] { white-space: normal; } - .chart-title-row { - align-items: stretch; - flex-direction: column; - } - - .chart-zoom-controls { - align-self: flex-end; - } } @media (max-width: 360px) { @@ -5973,7 +5569,6 @@ textarea[aria-invalid="true"] { flex: 0 0 auto; } - .queue-overview-button { display: inline-flex; align-items: center; @@ -7577,7 +7172,6 @@ body.flow-editor-open { border-top: 3px solid var(--accent); } - .flow-enabled { white-space: nowrap; } @@ -8744,7 +8338,6 @@ textarea:focus { color: var(--accent); } - html, body { max-width: 100%; @@ -8823,62 +8416,60 @@ body.flow-editor-open { background: var(--surface-2); } -body.chart-fullscreen-open { - overflow: hidden; +@media (max-width: 760px) { + .chart-fullscreen-button { + display: none; + } } -body.chart-fullscreen-open::before { - content: ""; - position: fixed; - inset: 0; - z-index: 139; - background: rgba(0, 0, 0, .68); - backdrop-filter: blur(3px); -} +@media (min-width: 761px) { + body.chart-fullscreen-open { + overflow: hidden; + } -.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 { + position: fixed; + inset: 0; + z-index: 140; + width: 100vw; + height: 100vh; + height: 100dvh; + max-width: none; + margin: 0; + padding: 16px; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 10px; + overflow: hidden; + border: 0; + border-radius: 0; + background: var(--surface); + box-shadow: none; + } -.history-chart-card.chart-fullscreen-fallback .chart-title-row { - margin: 0; -} + .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-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 .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; + .history-chart-card.chart-fullscreen-fallback .legend { + max-height: 88px; + margin: 0; + overflow: auto; + } } @media (max-width:760px) { @@ -8921,62 +8512,6 @@ body.chart-fullscreen-open::before { 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; - } } .manual-zone-blocked { @@ -9572,7 +9107,6 @@ body.chart-fullscreen-open::before { } } - #controlPlan .plan-card { text-align: left; } @@ -9706,65 +9240,6 @@ body.chart-fullscreen-open::before { } } -@media (max-width: 760px) { - .history-chart-card.chart-fullscreen-fallback { - width: calc(100vw - 8px); - height: calc(100dvh - 8px); - padding: 8px; - display: flex; - flex-direction: column; - gap: 8px; - overflow: hidden; - border-radius: 12px; - } - - .history-chart-card.chart-fullscreen-fallback .chart-title-row { - flex: 0 0 auto; - margin: 0; - } - - .history-chart-card.chart-fullscreen-fallback .chart-title-row h3 { - font-size: 16px; - line-height: 1.25; - } - - .history-chart-card.chart-fullscreen-fallback .chart-wrap { - flex: 0 0 auto; - width: 100%; - height: clamp(280px, 45dvh, 430px); - min-height: 0; - max-height: 430px; - overflow-x: auto; - overflow-y: hidden; - } - - .history-chart-card.chart-fullscreen-fallback .legend { - display: flex; - flex: 0 0 auto; - flex-wrap: nowrap; - gap: 5px; - width: 100%; - max-height: 42px; - margin: 0; - padding-bottom: 2px; - overflow-x: auto; - overflow-y: hidden; - scrollbar-width: none; - overscroll-behavior-inline: contain; - } - - .history-chart-card.chart-fullscreen-fallback .legend::-webkit-scrollbar { - display: none; - } - - .history-chart-card.chart-fullscreen-fallback .legend-item { - flex: 0 0 auto; - min-height: 32px; - padding: 4px 7px; - white-space: nowrap; - } -} - /* In phone landscape there is enough width: keep the full Fit label instead of clipping it. */ @media (orientation: landscape) and (max-height: 700px) and (min-width: 500px) { .flow-fit-button { @@ -9785,9 +9260,3 @@ body.chart-fullscreen-open::before { } } -@media (max-width: 760px) { - .history-chart-card.chart-fullscreen-fallback .chart-wrap { - height: clamp(260px, 38dvh, 360px); - max-height: 360px; - } -}