From 440c7c3a8d8a15178796b67d0ba97429d1c8d9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 25 Aug 2026 09:43:28 +0200 Subject: [PATCH] v0.6.6 --- BUILD_REPORT.md | 13 ++- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 28 +++--- README.md | 4 +- docs/API.md | 2 +- home-assistant/README.md | 1 + .../gree_controller/manifest.json | 2 +- lang/en.json | 2 +- lang/pl.json | 2 +- src/api.rs | 95 ++++++++++++++----- web/app.js | 8 +- web/index.html | 4 +- web/styles.css | 6 +- web/sw.js | 2 +- 15 files changed, 118 insertions(+), 55 deletions(-) diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 11933d5..1ae2734 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,8 +1,8 @@ -# GREE Controller v0.6.5 - build and validation report +# GREE Controller v0.6.6 - build and validation report ## Scope -Version 0.6.5 includes the 0.6.4 changes described below plus the whole-house thermostat semantics, master power control, Home Assistant house controls and LAN response-time reporting documented in this report. +Version 0.6.6 includes the 0.6.5 changes described below plus synchronized whole-house activation: selecting a global operating mode or work profile now re-enables master power and powers on all enabled units. The global power panel is also placed above the house climate controls. ## Implemented changes @@ -130,3 +130,12 @@ Validation performed in the build environment: - Home Assistant now exposes controller-level **Thermostat mode** and **Work profile** select entities plus an **All air conditioners** master-power switch, backed by dedicated restricted HA endpoints. - Device state stores the latest successful GREE communication round-trip time. **LAN network -> Devices** displays it in milliseconds next to the IP address and protocol. - PWA cache key advanced to `gree-controller-v065`. + + +## 0.6.6 synchronized whole-house activation + +- The global power panel is displayed above **House climate mode**. +- Selecting whole-house Cooling or Heating clears a previous global power-off and powers on all enabled units. +- Selecting Auto, Comfort, Sleep or Away as a whole-house profile does the same. +- House mode `off` / **Do not control** remains non-invasive and does not alter master power. +- The behavior is implemented in the shared backend endpoints, so Home Assistant house-mode/profile controls follow the same rules. diff --git a/Cargo.lock b/Cargo.lock index 9df77cd..b3c88ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.6.5" +version = "0.6.6" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index b254941..bacb555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.6.5" +version = "0.6.6" 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 6cc0d59..b6d9efa 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,19 +1,19 @@ d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -c6ea33a11a287f159c6e4c5f19967d8599146692cd576cfe6b8ccacddadd558a ./BUILD_REPORT.md -c9673a32218f054f3d3d3b7f89cba3f0aa75f7078558e43ed3adff064e65d209 ./Cargo.lock -735f566fe24949e8a4fae1bebd42d12e91a91cb99a36b114803f0cf0b93212a6 ./Cargo.toml +c5c9d9dad288bd9ec4695db7e4c1003ddd39a6cdf70073362dd84c7a87da57f8 ./BUILD_REPORT.md +dae36dbeeb9f26dce98eefc232c47b2fcbd5b75c9b896943990fa65b642c3638 ./Cargo.lock +2a20c8523878f420c0f3d4df542b60220d7b645da8ed2acdc71d7328d721013e ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -90fe7b3c18f2ce0d244b8134b73841d3b416d33f7c13d3da107517a124c58d7e ./README.md +0e702e55efa6445d959c03c54dff71b1e56317b6fa9de28c52ef34a38c65e9fc ./README.md f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs -6e0e5eb103c4acdaeb04c224fdd49e7ec051c667732d131439a165ac1739b713 ./docs/API.md +1aeca9e23d3a4fcbe3214acafdc2a33cdffa0ad46efb5ead4477a2ebc3a4286c ./docs/API.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md 7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md 10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md 19e6532f21c59b93c77863e8a8ac3a7d9abaf869f18e30401b7d0975b404b06e ./docs/PROJECT_SPEC.md 7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md 28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md -248570ceff0e020c9d81e8b9e1622ce33f3350f51a04371a9f4fc3f8fa13f62c ./home-assistant/README.md +8942e83eabe4edc9728594236b61e3c2be39b8bb46b66c19c4eff7ec7fe80716 ./home-assistant/README.md f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py 001215412d77abd393f34c414ff6d0ebbaf419f140d76037b7f114a7955df6c3 ./home-assistant/custom_components/gree_controller/api.py abcca32b090e5a5d02574a3a340e97321f9879ae6c94ef12eb1f42d967eae2d5 ./home-assistant/custom_components/gree_controller/climate.py @@ -23,7 +23,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./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 -8666eebd03276d6649104dcf538b79073f4f50a51b1440bcbad0a23c09b0f78b ./home-assistant/custom_components/gree_controller/manifest.json +52c67fb50b8e9fec5c60af20df6db3385cc40afe08e57b9ea1776ab5fefdaf70 ./home-assistant/custom_components/gree_controller/manifest.json d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./home-assistant/custom_components/gree_controller/number.py 459233ccefa7827fc1c6887146d8a69e01149a497635fd5e1802c220f418eb28 ./home-assistant/custom_components/gree_controller/select.py ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./home-assistant/custom_components/gree_controller/sensor.py @@ -32,8 +32,8 @@ ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./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 -997c7a91017b5d51b3e44016d21252b37d302f95ba0fb5802f3d8fd116687d6f ./lang/en.json -33f6935ad1fe72ea0e6893e230ff95565388410fb0fbfe974b34610d789c5d2b ./lang/pl.json +3b147813b7ad9d44c99ec4b2c062de77f914ae04538abc6cfaa9e5451fc6a83c ./lang/en.json +c301de561c5999031b7a09101a67ab5c7ee49118fe181ad152741c6c2ea01272 ./lang/pl.json 028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh @@ -46,7 +46,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -f8d1349236819fe0f0253df5269353245f0cacf5b29614ac79c2317c16943636 ./src/api.rs +f3d38b84a241cf0dd14ae8f8db7f1ef972734c1f2acaeb8d0bd7997c6e8f05bd ./src/api.rs 32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs 8ec14c01036c73f28cdb330d696f0d3573198e7ea4d03ba6bf9cc1d34ec6c7a8 ./src/engine.rs @@ -62,10 +62,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service -53b94be090889a8f709b16b0f88e9e9c732427151de3cb740c1f16144dec37b4 ./web/app.js +8a5471859605115780e1a1cc3d07345698c846c642a9a604977ae2ca73cec0ec ./web/app.js e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -59e85721942f533243d12491e201dbfedef859dc752b7f0c7c37405705832d2d ./web/index.html +3d47ede16e8652f2b36223ca26271ca302d8a1c62030f18a47b461da9c1bf47f ./web/index.html fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest -a9643913bf5ceba3b0940fff583350f5bb853e7d0421296a9661a4e3e256bde1 ./web/styles.css -e804540ecc928c3f29a8e71fdae68a6e0a36eb0c0698b1ca22cf8f42a888d61c ./web/sw.js +3796167c55146cf549a6df8b7057072ba4731ed667f9691a35a21832139169cd ./web/styles.css +aba62a4b8535e59fd78650142e96c7234ef744e02f28178923b40b5188cb0265 ./web/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js diff --git a/README.md b/README.md index ac90c4d..e605d5c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud. -Current version: **0.6.5**. +Current version: **0.6.6**. ## Highlights @@ -46,7 +46,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do On Debian, Ubuntu or an LXC container: ```bash -unzip gree-controller-v0.6.5.zip +unzip gree-controller-v0.6.6.zip cd gree-controller chmod +x scripts/*.sh ./scripts/dev.sh diff --git a/docs/API.md b/docs/API.md index 370656b..86f59aa 100644 --- a/docs/API.md +++ b/docs/API.md @@ -79,7 +79,7 @@ curl -X POST "$BASE/api/house/preset" -H "$AUTH" -H 'Content-Type: application/j -d '{"preset":"sleep"}' ``` -House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates temporary per-zone overrides that expire at each zone's next schedule boundary. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation. +House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates temporary per-zone overrides that expire at each zone's next schedule boundary. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation. Choosing whole-house `cool`/`heat` or any whole-house preset is treated as an explicit whole-house activation: master power is set back to on and all enabled units are powered on. Choosing house mode `off` does not change master power. A zone stores separate profile temperatures for both seasons: diff --git a/home-assistant/README.md b/home-assistant/README.md index d488031..e7dba1d 100644 --- a/home-assistant/README.md +++ b/home-assistant/README.md @@ -132,6 +132,7 @@ The integration also exposes three controller-level entities on the **GREE Contr - **Thermostat mode** (`select`) — Cooling, Heating or **Do not control**. Do not control pauses house-level thermostat commands but leaves direct device control and explicit per-zone Heat/Cool overrides untouched. - **Work profile** (`select`) — Auto schedule, Comfort, Sleep or Away. If zones have mixed manual profiles, the select has no single current option until a whole-house profile is chosen again. +- Choosing **Cooling/Heating** in the thermostat-mode select or any whole-house **Work profile** is treated as explicit whole-house activation: it switches **All air conditioners** back on. **Do not control** does not change master power. - **All air conditioners** (`switch`) — separate master power. Turning it off powers every enabled unit down and prevents zones/controller automations from restarting them. Turning it on powers all enabled units on again without changing the selected thermostat mode or profile. These controls use the Home Assistant integration token and the dedicated `/api/integrations/home-assistant/house/*` endpoints. diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index 9788eb5..4fec543 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.6.5", + "version": "0.6.6", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/lang/en.json b/lang/en.json index 990afde..57acba9 100644 --- a/lang/en.json +++ b/lang/en.json @@ -534,7 +534,7 @@ "house.noControl": "Do not control", "house.powerAllEyebrow": "Global power", "house.powerAllTitle": "All air conditioners", - "house.powerAllHint": "Separate master power for all enabled units. “Turn all off” prevents zones and automations from restarting them without changing the selected thermostat mode.", + "house.powerAllHint": "Master power for all enabled units. “Turn all off” prevents them from restarting. Choosing whole-house Cooling/Heating or an Auto/Comfort/Sleep/Away profile automatically switches master power back on.", "house.powerOnAll": "Turn all on", "house.powerOffAll": "Turn all off", "house.powerOnDone": "All air conditioners turned on", diff --git a/lang/pl.json b/lang/pl.json index 845dd69..3454135 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -534,7 +534,7 @@ "house.noControl": "Nie steruj", "house.powerAllEyebrow": "Zasilanie globalne", "house.powerAllTitle": "Wszystkie klimatyzatory", - "house.powerAllHint": "Osobne nadrzędne zasilanie wszystkich aktywnych jednostek. „Wyłącz wszystkie” blokuje ponowne uruchamianie przez strefy i automatyzacje, ale nie zmienia wybranego trybu termostatu.", + "house.powerAllHint": "Nadrzędne zasilanie wszystkich aktywnych jednostek. „Wyłącz wszystkie” blokuje ich ponowne uruchamianie. Globalny wybór Chłodzenia/Grzania albo profilu Auto/Komfort/Sen/Poza domem automatycznie ustawia „Włącz wszystkie”.", "house.powerOnAll": "Włącz wszystkie", "house.powerOffAll": "Wyłącz wszystkie", "house.powerOnDone": "Włączono wszystkie klimatyzatory", diff --git a/src/api.rs b/src/api.rs index 550c045..f677ed1 100644 --- a/src/api.rs +++ b/src/api.rs @@ -594,16 +594,55 @@ async fn update_zone_control(State(state): State, Path(id): Path Result, AppError> { + let mut failed = Vec::new(); + for device in state.db.list_devices()? { + if !device.enabled || device.power == power { continue; } + let command = DeviceCommand { power: Some(power), ..Default::default() }; + if let Err(err) = engine::send_command(state, &device.id, command).await { + state.log("error", "house.power_all_error", &err.to_string(), json!({ + "device_id": device.id, + "device_name": device.name, + "power": power, + "source": source, + })); + failed.push(json!({ + "device_id": device.id, + "device_name": device.name, + "error": err.to_string(), + })); + } + } + Ok(failed) +} + async fn update_house_control(State(state): State, Json(input): Json) -> Result, AppError> { if !matches!(input.mode.as_str(), "cool" | "heat" | "off") { return Err(AppError::BadRequest("house mode must be cool, heat or off".into())); } - let mut settings = state.settings.write().await; - settings.house_mode = input.mode; - state.db.save_runtime_settings(&settings)?; - let payload = public_settings(&settings); + let mode = input.mode; + let activate_all = mode != "off"; + let payload = { + let mut settings = state.settings.write().await; + settings.house_mode = mode.clone(); + // Choosing a real whole-house operating mode is an explicit request to run the + // house climate. It therefore clears a previous global power-off. "off" keeps + // its separate meaning: do not control, without changing master power. + if activate_all { settings.house_power_enabled = true; } + state.db.save_runtime_settings(&settings)?; + public_settings(&settings) + }; state.broadcast("settings.updated", payload.clone()); - state.log("info", "house.mode", &format!("House mode set to {}", settings.house_mode), json!({"mode": settings.house_mode})); + if activate_all { + let failed = command_all_enabled_devices_power(&state, true, "house_mode").await?; + if !failed.is_empty() { + state.log("warn", "house.mode_power_partial", "House mode enabled master power, but some devices could not be powered on", json!({ + "mode": mode, + "failed": failed.len(), + })); + } + } + state.log("info", "house.mode", &format!("House mode set to {}", mode), json!({"mode": mode, "master_power_enabled": activate_all})); Ok(Json(payload)) } @@ -623,23 +662,7 @@ async fn update_house_power(State(state): State, Json(input): Json, Json(input): Json, Json(input): Json { return; } if (action === 'house-preset') { - try { const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); app.zones=result.zones||app.zones; renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); toast(tr('house.presetUpdated')); } + try { + const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); + app.settings=result.settings||app.settings; app.zones=result.zones||app.zones; app.devices=result.devices||app.devices; + renderAll(); scheduleControlPlanLoad(); + const failed=Array.isArray(result.failed)?result.failed.length:0; + if(failed) toast(tr('house.powerPartial',{count:failed}),true); else toast(tr('house.presetUpdated')); + } catch(error){ toast(error.message,true); } return; } if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; } diff --git a/web/index.html b/web/index.html index 079f22a..954a618 100644 --- a/web/index.html +++ b/web/index.html @@ -37,11 +37,11 @@
--°C
-
-
Global power

All air conditioners

Direct power control for all enabled units, independent from thermostat presets.

+
Global power

All air conditioners

Master power for all enabled units. Whole-house mode or profile selection automatically switches it back on.

+
Control plan

What happens next

Zones

Quick thermostats

diff --git a/web/styles.css b/web/styles.css index 4b79a71..893efff 100644 --- a/web/styles.css +++ b/web/styles.css @@ -298,7 +298,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; } .mode-row, .fan-row, .preset-row { scrollbar-width: none; } .mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; } -.house-climate { display: grid; gap: 16px; margin: -15px 0 12px; } +.house-climate { display: grid; gap: 16px; margin: 0 0 12px; } .house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .house-climate-head h3 { margin: 2px 0 4px; font-size: 20px; } .house-climate-head p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; } @@ -325,7 +325,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; } .plan-loading { grid-column: 1/-1; padding: 18px; color: var(--muted); } .house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .house-mode-row button { min-height: 44px; color: var(--muted); background: var(--surface-muted); } -.house-power-panel { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 34px; } +.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; gap: 8px; } @@ -360,7 +360,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; } .warning-note { color: var(--warning); } @media (max-width: 620px) { - .house-climate { margin-top: -20px; } + .house-power-panel { margin-top: -20px; } .house-climate-head { align-items: flex-start; } .house-climate-head p { max-width: 220px; } .house-power-panel { align-items: stretch; flex-direction: column; } diff --git a/web/sw.js b/web/sw.js index 4474670..b89e4c0 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'gree-controller-v065'; +const CACHE = 'gree-controller-v066'; const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, ''); const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/'; const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];