This commit is contained in:
Mateusz Gruszczyński
2026-08-23 23:44:35 +02:00
parent f569b7db14
commit 6e3075fae5
13 changed files with 313 additions and 591 deletions
+32 -32
View File
@@ -1,49 +1,49 @@
# GREE Controller v0.4.2 - build and validation report
# GREE Controller v0.4.3 - build and validation report
## Scope
Version 0.4.2 expands climate history and removes the Node/Tailwind build pipeline.
Version 0.4.3 restores the classic rounded GREE Controller UI while keeping the neutral light/dark palette and green accents introduced in the previous release. It also removes three Rust dead-code warnings reported by the LXC release build.
### History changes
## UI changes
- Added a dedicated `zone_readings` SQLite table.
- History samples include GREE indoor temperature, optional Home Assistant room temperature, calculated control temperature, effective profile target, device setpoint, Home Assistant outdoor temperature, power, HVAC mode, fan speed, demand, control source and active preset.
- Zone history is sampled no faster than every 15 seconds (or the configured device poll interval when longer) to avoid unnecessary SQLite growth.
- Added `GET /api/history` for one zone or all zones.
- History UI now supports a detailed per-zone view and common multi-zone comparison charts.
- Existing `readings` data and tables remain intact; the new table is created automatically on startup.
- Restored the classic large rounded cards and panels.
- Restored circular thermostat +/- and power controls.
- Restored compact desktop navigation and five-item mobile bottom navigation.
- Kept the v0.4.2 climate-history functionality and multi-series charts.
- Kept neutral dark/light surfaces.
- Dark accent: `#3ecf8e`.
- Light accent: `#24b47e`.
- UI is plain committed CSS in `web/styles.css`; no frontend package manager or CSS build step is used.
- Theme-aware scrollbars remain enabled and compact horizontal control scrollbars remain hidden.
### Web asset changes
## Rust warning cleanup
- `web/styles.css` is now the only CSS source used by the application.
- Removed `package.json`, `tailwind.config.js`, `web/tailwind.input.css` and `scripts/build-web.sh`.
- Removed all Node/npm/Tailwind build hooks from `dev.sh`, `install.sh` and `update.sh`.
- The CSS remains fully local and is embedded in the Rust binary with `include_str!`.
Removed code that was superseded by bucketed zone history:
## Validation performed
- `Db::list_zone_readings`
- `queries::LIST_ZONE_READINGS_BY_ZONE`
- `queries::LIST_ZONE_READINGS_ALL`
Passed in the packaging environment:
The active history path is `Db::list_zone_history` with the bucketed SQL queries.
- JavaScript syntax: `node --check web/app.js` and `web/sw.js`.
- JSON syntax for EN/PL language files and the PWA manifest.
- EN/PL translation-key parity (300 keys each).
- Shell syntax for all scripts in `scripts/`.
- Python syntax for the Home Assistant migration generator.
- HTML parsing and CSS brace-balance checks.
- SQLite schema creation plus insert/throttle/read tests for `zone_readings`.
- SQL centralization check: new SQL statements are in `src/queries.rs`.
- No Node/npm/Tailwind build files remain in the release tree.
## Validation performed in packaging environment
## Rust compilation
- HTML structure sanity checks.
- JavaScript syntax check with Node's parser only; Node is not part of the application/runtime.
- JSON validation for language files and HA manifest.
- Shell syntax checks for `scripts/*.sh`.
- Python syntax check for migration helper.
- Verified no `list_zone_readings` or `LIST_ZONE_READINGS_*` references remain.
- Verified no frontend build/package files are required.
- ZIP integrity test and SHA-256 manifest generation.
The packaging environment does not contain Cargo/Rust, so a full Rust compile could not be executed here. The LXC installer/updater still runs `cargo test --all-targets` before replacing the installed binary, unless `--skip-tests` is explicitly used.
A Rust toolchain is not available in the packaging environment, so the authoritative Rust tests and warning check run on the LXC through `scripts/update.sh` before installation.
## Recommended LXC update
Expected LXC validation:
```bash
unzip gree-controller-v0.4.2.zip
cd gree-controller
sudo ./scripts/update.sh
cargo test --all-targets
cargo build --release
```
The updater preserves `/etc/gree-controller.env` and `/var/lib/gree-controller/gree-controller.db` and rolls back automatically if the new service fails its health check.
The three warnings supplied from v0.4.2 have been removed at source rather than suppressed.
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.4.2"
version = "0.4.3"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+12 -11
View File
@@ -1,15 +1,15 @@
da945a594f47b2b0c326cc0275b10a23cb509f545a1841ba9c57f00086e553ff ./.env.example
17780b6416a709efae772c37fb1151a35e51e6e8e6f428566d2f3eb308a4c58d ./.gitignore
c7adcd3ac665fe6a769519a2eb61dd96a494146c05708cab557238620e25368d ./BUILD_REPORT.md
963046cc499a783fd381006051513c0f7138097f793e7d2e8c44417192dc4b99 ./Cargo.toml
d8981625e50f5df8633f17c82fd24f8f0ee1f7fdc85feda77d82aa910fcaa214 ./BUILD_REPORT.md
f0a46de96da26d647d40de85670473502457e8527d219e4a4b94f12faec653bd ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
53c95efcdd17d4d21aefbed2154d6eef3697d3e9757a11fcce72948693cfb84b ./README.md
c24b1043c8e362bb1478fb7512d324c30b20b17658a34aaabc6ea99c34e6c69b ./README.md
a4fa9bfee9735ed8ed95ea31456e0cce503d82502ae3f550108ffca51b0f0c3d ./build.rs
0fb1606ecf67d7c73c53c4a9011d7ece4f2aa3b678eefeb446c4af5c28f91295 ./docs/API.md
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
ccfc7bc12d2b7e3b8d85c395e2eb37c991c2c3a3f901759cc0d2b5b0e4304b82 ./docs/PROJECT_SPEC.md
8fa2f24618026d9d5c39343c0a6a402c6329c384d76df63297ba026dcea11ee0 ./docs/PROJECT_SPEC.md
33214270b96ac4c64e3db41c11e54158792b4a87ee5668c651bad571766b591a ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
e0cf725c9f84be51cdbd83a5ab12b2b8288f90b36623cb4671cefcbf04378504 ./home-assistant/custom_components/gree_controller/api.py
@@ -18,14 +18,15 @@ fde31b8e020fd36be2d9d9b1e554254f5da8a1cc593240ebb8d78820154dd790 ./home-assista
e1821b74859bc40773a6ee39e6ccc9650980b62af50d6426b46cdb3e3a90d200 ./home-assistant/custom_components/gree_controller/const.py
2d27d7cb67c53e819b99a0302cd0bd339c27e3e2dfc85e07d30c31f505f740b5 ./home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
ec45d55405c18631a66dc02baa924011dcc42d1e51bf5e209677ec6ce7a8282b ./home-assistant/custom_components/gree_controller/manifest.json
e361220da4193b6c02f06718aa6240b6af54f1c6330916dd66f0ea5ea8f26542 ./home-assistant/custom_components/gree_controller/manifest.json
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
c871974574a31e6c90e3572c6cae73fd014a9b2dffd486936bcf1a93727eb95d ./lang/en.json
27e7edd19b2010e6ab71ccb397cf94e99e390d9caca3eac83000d558d871b990 ./lang/pl.json
896c553a9e2a6de6ce6f69ae50b3eb78c0803bbc9573eb841440d98955eec919 ./scripts/README.md
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
896caae04468f743d85d011638a0683253d8271c0db8365aea9bcdc646795cf1 ./scripts/__pycache__/generate_ha_migration.cpython-313.pyc
3640e34a1ed5a93be3e96610848b16b8e748d5ca95975ac7e95d38cbf32695a8 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
dbb92ddc27b8724faf709983e4feecd3f188052b2cba39daba96d4bc16914df5 ./scripts/dev.sh
@@ -38,7 +39,7 @@ b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smok
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
9620b0d32a24ec8111b46f34f4502565dfc601896ef92ba9be74fc25a4bd020f ./src/api.rs
5138f9762540da497360667bd93e5e7274515a37baf3ca3c19c38b23a57c9e2b ./src/config.rs
c68ec99a0dd26d3a49a9d14974af3bdca106aaebaf737a9d76a811850d7ecb53 ./src/db.rs
53f4d02b6e7259525c45766a06002f2094ce51bc0b6ec1a04b2fe039cd8714f7 ./src/db.rs
3f5ffc1ff1fabab5952506d26349f37c23ea270e1351f0e8579a920c1bbc0f84 ./src/engine.rs
ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs
584a5dad5d52cafa0ab9d0883c501c7bc5d3126f515ace98a261a78a6d714c3a ./src/home_assistant.rs
@@ -47,12 +48,12 @@ ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
b66ae7c0186b6cc3939d922b0a83f763fd670662c170c97cebca8235d99f663c ./src/protocol/gree.rs
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
a27f64422712934b6d550a7dc6e520ff9f89099ecbcab2594169f0bd97052318 ./src/queries.rs
6ca5aea6efd4e8c1c9ec0ed81289c0f964a3f82117859439d62750c250a31822 ./src/queries.rs
3bd56018114a7199082e9e9d973107b73cb8259693a80743f4022364cda62095 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
7b2f7054ab62d33fe9f43c9cab5056b3de2e4c3196e168f8669a640f5bf2d77b ./web/app.js
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
07a9405ad577d56fda37c51ad0d8f4ff7bff176727ccea45eb52cc8be23af8e5 ./web/index.html
c5d82744adbd17e00b21f48abf279f3362345ac12f7d0fb769f21cb97b7c0b41 ./web/index.html
6db5f01ef1284c6de175c411fb7014a7b312436bc5964ee8086ed94650047b8c ./web/manifest.webmanifest
b2889d002d1c345b554a69279081754ed4b984bb98431819a9f384f79b297b8d ./web/styles.css
ae6af783cd2830e899494eda419b2310496ef75301b88d8ef6fa5ad5ca1e886b ./web/sw.js
973c283e1c69c2c050ca863c6f190429280d06d6fc6b6c93648c4678076aeeb1 ./web/styles.css
187af1d3b5c6e26c06a7b3b87c31fa0abe296343f56d4fe167fbd8377b0ee02e ./web/sw.js
+4 -4
View File
@@ -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.4.2**.
Current version: **0.4.3**.
## Highlights
@@ -39,7 +39,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.4.2.zip
unzip gree-controller-v0.4.3.zip
cd gree-controller
chmod +x scripts/*.sh
./scripts/dev.sh
@@ -85,11 +85,11 @@ Appearance selector:
- Light,
- Dark.
The selected appearance is stored in the `gree_controller_theme` cookie. Version 0.4.2 refreshes the interface with a Supabase-inspired dashboard language: layered neutral surfaces, compact controls, thin borders, small radii, a restrained green brand accent, a desktop sidebar, mobile bottom navigation, and theme-aware scrollbars. The design remains original to GREE Controller and does not copy Supabase branding or application code.
The selected appearance is stored in the `gree_controller_theme` cookie. Version 0.4.3 restores the classic GREE Controller interface: large rounded cards, circular thermostat controls, compact desktop navigation and mobile bottom navigation. It keeps the newer neutral dark/light palette and green accent (`#3ecf8e` in dark mode and `#24b47e` in light mode).
### Static offline CSS
The Supabase/Tailwind-inspired interface is shipped as a normal, committed `web/styles.css` file and embedded directly into the Rust binary. There is no Node.js, npm, Tailwind CLI, CDN or CSS build step. Editing the UI means editing `web/styles.css` and `web/index.html` directly, then rebuilding the Rust application.
The interface is plain static CSS. `web/styles.css` is committed with the project and embedded directly into the Rust binary. There is no frontend package manager, CDN, or CSS build step. Edit `web/styles.css` and `web/index.html` directly, then rebuild the Rust application.
## Connecting a physical GREE device
+3 -3
View File
@@ -175,11 +175,11 @@ The zone controller supports `device`, `combined`, and `home_assistant` temperat
- Every SQLite statement and schema definition is centralized in `src/queries.rs`; database/domain code must not embed SQL strings elsewhere.
## Web UI design system (v0.4.2)
## Web UI design system (v0.4.3)
The embedded UI uses a compact Supabase-inspired dashboard language while retaining GREE Controller branding and information architecture. Light, Dark and System themes are driven by CSS design tokens. The visual system favors neutral layered surfaces, thin borders, 6-8 px radii, compact controls, a restrained green accent, no decorative shadows, a desktop sidebar and mobile bottom navigation.
The embedded UI uses the classic GREE Controller visual language: large rounded surfaces, circular thermostat controls, comfortable spacing, compact desktop navigation and mobile bottom navigation. Light, Dark and System themes use neutral surfaces with a restrained green accent.
The UI uses a Supabase/Tailwind-inspired visual language but ships as a normal static `web/styles.css` file embedded into the Rust binary. There is no Node.js, npm, Tailwind CLI, CDN, or CSS build pipeline.
The UI ships as a normal static `web/styles.css` file embedded into the Rust binary. There is no frontend package manager, CDN, or CSS build pipeline.
### Rich zone history
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.4.2",
"version": "0.4.3",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
+1 -1
View File
@@ -16,4 +16,4 @@ All operator-facing scripts live in this directory.
- `configure-gree-network.sh <interface>` — configures a dedicated GREE NIC, automatic subnet broadcast, disables the simulator, and restarts the service.
- `network-debug.sh <interface> [broadcast-ip]` — prints interface/routing diagnostics and a tcpdump command.
The running controller never uses a CDN or Node.js. `web/styles.css` is a normal static CSS file embedded into the Rust binary; there is no CSS build step.
The running controller uses only the committed `web/styles.css` embedded into the Rust binary. There is no frontend dependency installation, CDN, or CSS build step.
-16
View File
@@ -270,22 +270,6 @@ impl Db {
Ok(changed > 0)
}
pub fn list_zone_readings(&self, zone_id: Option<&str>, since: DateTime<Utc>, limit: u32) -> Result<Vec<ZoneReading>> {
let conn = self.lock()?;
let limit = limit.clamp(1, 20_000) as i64;
let mut rows_out = Vec::new();
if let Some(zone_id) = zone_id {
let mut stmt = conn.prepare(queries::LIST_ZONE_READINGS_BY_ZONE)?;
let rows = stmt.query_map(params![zone_id, since.to_rfc3339(), limit], Self::map_zone_reading)?;
for row in rows { rows_out.push(row?); }
} else {
let mut stmt = conn.prepare(queries::LIST_ZONE_READINGS_ALL)?;
let rows = stmt.query_map(params![since.to_rfc3339(), limit], Self::map_zone_reading)?;
for row in rows { rows_out.push(row?); }
}
Ok(rows_out)
}
pub fn list_zone_history(&self, zone_id: Option<&str>, since: DateTime<Utc>, bucket_seconds: i64, limit: u32) -> Result<Vec<ZoneReading>> {
let conn = self.lock()?;
let bucket_seconds = bucket_seconds.max(1);
-19
View File
@@ -212,25 +212,6 @@ WHERE NOT EXISTS (
)
"#;
pub const LIST_ZONE_READINGS_BY_ZONE: &str = r#"
SELECT id,zone_id,device_id,timestamp,gree_temperature,external_temperature,
control_temperature,target_temperature,device_setpoint,outdoor_temperature,
power,mode,fan_speed,demand,control_source,active_preset
FROM zone_readings
WHERE zone_id=?1 AND timestamp>=?2
ORDER BY timestamp ASC
LIMIT ?3
"#;
pub const LIST_ZONE_READINGS_ALL: &str = r#"
SELECT id,zone_id,device_id,timestamp,gree_temperature,external_temperature,
control_temperature,target_temperature,device_setpoint,outdoor_temperature,
power,mode,fan_speed,demand,control_source,active_preset
FROM zone_readings
WHERE timestamp>=?1
ORDER BY timestamp ASC
LIMIT ?2
"#;
pub const LIST_ZONE_HISTORY_BY_ZONE_BUCKETED: &str = r#"
SELECT MIN(id),zone_id,MAX(device_id),MIN(timestamp),
+6 -10
View File
@@ -34,7 +34,7 @@
<option value="light" data-i18n="theme.light">Light</option>
<option value="dark" data-i18n="theme.dark">Dark</option>
</select>
<button class="icon-button" id="refreshButton" data-i18n-title="actions.refresh" data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.34 5.66"/><path d="M20 4v7h-7"/></svg></button>
<button class="icon-button" id="refreshButton" data-i18n-title="actions.refresh" data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh"></button>
<button class="primary compact" id="discoverButton" data-i18n="actions.discover">Discover</button>
</div>
</header>
@@ -155,15 +155,11 @@
</main>
<nav class="bottom-nav" data-i18n-aria="nav.navigation" aria-label="Navigation">
<button class="active" data-nav="dashboard"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5"/><path d="M5.5 9.5V21h13V9.5"/><path d="M9 21v-7h6v7"/></svg></span><b data-i18n="nav.dashboard">Dashboard</b></button>
<button data-nav="devices"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h4"/></svg></span><b data-i18n="nav.devices">Devices</b></button>
<button data-nav="zones"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg></span><b data-i18n="nav.zones">Zones</b></button>
<button data-nav="history"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path d="M4 19V5"/><path d="M4 19h16"/><path d="m7 15 4-4 3 2 5-6"/></svg></span><b data-i18n="nav.history">History</b></button>
<button class="desktop-nav-only" data-nav="schedules"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18"/></svg></span><b data-i18n="nav.schedules">Schedules</b></button>
<button class="desktop-nav-only" data-nav="automations"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path d="m13 2-8 12h7l-1 8 8-12h-7z"/></svg></span><b data-i18n="nav.automations">Automations</b></button>
<button class="desktop-nav-only" data-nav="settings"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.86 2.86-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.08V21h-4v-.08A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.86-2.86.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.08-.4H3v-4h.08A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06L7.06 3.8l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.08V3h4v.08A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.86 2.86-.06.06A1.7 1.7 0 0 0 19.4 9c.36.3.7.65 1 1 .2.3.4.65.4 1v2c0 .35-.2.7-.4 1-.3.35-.64.7-1 1z"/></svg></span><b data-i18n="nav.settings">Settings</b></button>
<button class="desktop-nav-only" data-nav="logs"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg></span><b data-i18n="nav.logs">Events</b></button>
<button data-nav="more"><span><svg viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg></span><b data-i18n="nav.more">More</b></button>
<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 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>
</nav>
<dialog id="moreDialog" class="sheet">
+251 -491
View File
@@ -16,6 +16,7 @@
--text-soft: #d4d4d4;
--muted: #a1a1aa;
--muted-2: #737373;
--muted-strong: #b5b5bd;
--accent: #3ecf8e;
--accent-strong: #46d99a;
--accent-soft: rgba(62, 207, 142, .10);
@@ -30,17 +31,13 @@
--teal: #2dd4bf;
--purple: #a78bfa;
--orange: #fb923c;
--muted-strong: #b5b5bd;
--backdrop: rgba(0, 0, 0, .68);
--grid: rgba(255, 255, 255, .07);
--scroll-thumb: #444444;
--scroll-thumb-hover: #575757;
--radius: 8px;
--radius-sm: 6px;
--control-h: 38px;
--radius: 22px;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
:root[data-theme="light"] {
color-scheme: light;
--bg: #f8faf9;
@@ -59,6 +56,7 @@
--text-soft: #3f3f46;
--muted: #71717a;
--muted-2: #a1a1aa;
--muted-strong: #52525b;
--accent: #24b47e;
--accent-strong: #1ea672;
--accent-soft: rgba(36, 180, 126, .09);
@@ -73,541 +71,303 @@
--teal: #0f9f8f;
--purple: #7c3aed;
--orange: #c56b21;
--muted-strong: #52525b;
--backdrop: rgba(23, 23, 23, .32);
--grid: rgba(24, 24, 27, .08);
--scroll-thumb: #c8ccca;
--scroll-thumb-hover: #aeb3b0;
}
* { box-sizing: border-box; }
html { background: var(--bg); }
html, body { min-height: 100%; }
body {
margin: 0;
min-height: 100vh;
color: var(--text);
background: var(--bg);
font-size: 14px;
line-height: 1.45;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
button, input, select, textarea { font: inherit; }
html, body { background: var(--bg); }
body { margin: 0; min-height: 100vh; color: var(--text); }
button, input, select { font: inherit; }
button { cursor: pointer; }
button:disabled { opacity: .48; cursor: wait; }
a { color: var(--accent); }
::selection { color: var(--text); background: var(--accent-soft); }
/* Accessible focus treatment in the same restrained style as the rest of the UI. */
:where(button, input, select, textarea, summary):focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent);
outline-offset: 2px;
}
.topbar {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
min-height: 54px;
padding: 8px 14px;
border-bottom: 1px solid var(--line);
background: var(--nav-bg);
}
button:disabled { opacity: .5; cursor: wait; }
.topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 1rem; min-height: 72px; padding: 12px max(18px, env(safe-area-inset-left)); border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--nav-bg) 94%, transparent); backdrop-filter: blur(14px); }
.brand { display: flex; align-items: center; min-width: 0; }
.brand strong, .brand small { display: block; }
.brand strong { color: var(--text); font-size: 13px; font-weight: 650; letter-spacing: -.01em; white-space: nowrap; }
.brand small { margin-top: 1px; color: var(--muted); font-size: 10px; }
.brand.large { margin-bottom: 16px; }
.brand.large strong { font-size: 20px; }
.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; }
.toolbar-select {
width: auto;
min-width: 58px;
min-height: 34px;
padding: 6px 28px 6px 9px;
border-radius: var(--radius-sm);
font-size: 12px;
}
.theme-select { min-width: 92px; }
button {
min-height: 36px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 8px 12px;
color: var(--text-soft);
background: var(--surface);
font-size: 13px;
font-weight: 520;
transition: border-color .12s ease, background-color .12s ease, color .12s ease, opacity .12s ease;
}
button:hover { border-color: var(--line-strong); background: var(--surface-2); color: var(--text); }
button:active { background: var(--surface-3); }
.primary {
border-color: color-mix(in srgb, var(--accent) 74%, var(--line));
color: var(--accent-text);
background: var(--accent);
font-weight: 650;
}
.primary:hover { border-color: var(--accent-strong); color: var(--accent-text); background: var(--accent-strong); }
.secondary { border-color: var(--line); color: var(--text-soft); background: var(--surface); font-weight: 540; }
.secondary:hover { border-color: var(--line-strong); background: var(--surface-2); }
.brand strong { font-size: 15px; letter-spacing: -.01em; white-space: nowrap; }
.brand small { margin-top: 2px; color: var(--muted); font-size: 11px; }
.brand.large { margin-bottom: 1rem; }
.brand.large strong { font-size: 21px; }
.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 7px; }
.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; padding: 11px 15px; color: var(--text); background: var(--surface-2); transition: transform .15s ease, background .15s ease, opacity .15s ease; }
button:active { transform: scale(.97); }
.primary { background: var(--accent); color: var(--accent-text); font-weight: 800; }
.primary:hover { background: var(--accent-strong); }
.secondary { border: 1px solid var(--line); background: var(--surface-muted); font-weight: 650; }
.secondary:hover { background: var(--surface-2); }
.danger { color: var(--danger); }
.danger:hover { border-color: color-mix(in srgb, var(--danger) 38%, var(--line)); background: var(--danger-soft); color: var(--danger); }
.compact { min-height: 34px; padding: 7px 11px; }
.icon-button {
display: inline-grid;
place-items: center;
width: 34px;
min-width: 34px;
height: 34px;
min-height: 34px;
padding: 0;
color: var(--muted);
background: transparent;
}
.icon-button:hover { color: var(--text); background: var(--surface-2); }
.icon-button svg { width: 16px; height: 16px; }
main {
width: min(1280px, 100%);
margin: 0 auto;
padding: 28px 18px 102px;
}
.view { display: none; }
.view.active { display: block; animation: reveal .12s ease-out; }
@keyframes reveal { from { opacity: .65; transform: translateY(2px); } }
.compact { padding: 10px 14px; }
.icon-button { width: 40px; height: 40px; padding: 0; font-size: 22px; background: transparent; }
main { width: min(1180px, 100%); margin: 0 auto; padding: 24px 18px 108px; }
.view { display: none; animation: reveal .2s ease; }
.view.active { display: block; }
@keyframes reveal { from { opacity: 0; transform: translateY(5px); } }
h1, h2, h3, p { margin-top: 0; }
h1 { margin-bottom: 7px; font-size: clamp(25px, 5vw, 34px); line-height: 1.08; font-weight: 620; letter-spacing: -.035em; }
h2 { margin-bottom: 0; font-size: clamp(20px, 4vw, 25px); line-height: 1.15; font-weight: 610; letter-spacing: -.025em; }
h3 { margin-bottom: 9px; font-size: 15px; font-weight: 620; letter-spacing: -.01em; }
.eyebrow { color: var(--muted); font-size: 10px; font-weight: 650; letter-spacing: .08em; text-transform: uppercase; }
.lead { max-width: 760px; color: var(--muted); font-size: 13px; line-height: 1.6; }
.hero {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
min-height: 154px;
padding: 24px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.hero::after { display: none; }
.hero h1 { margin: 4px 0 7px; }
.hero p { margin: 0; color: var(--muted); font-size: 13px; }
.hero-temp { z-index: 1; white-space: nowrap; color: var(--text); font-size: clamp(48px, 9vw, 68px); font-weight: 310; line-height: 1; letter-spacing: -.065em; }
.hero-temp small { margin-left: 4px; color: var(--muted); font-size: .28em; font-weight: 550; letter-spacing: 0; }
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 10px 0 28px; }
.metric { padding: 14px 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.metric span { display: block; margin-bottom: 6px; color: var(--muted); font-size: 11px; }
.metric strong { color: var(--text); font-size: 21px; font-weight: 590; letter-spacing: -.025em; }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 14px; margin: 8px 0 14px; }
.section-heading > div { min-width: 0; }
.device-grid, .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr)); gap: 10px; }
h1 { margin-bottom: 8px; font-size: clamp(28px, 6vw, 44px); letter-spacing: -.045em; }
h2 { margin-bottom: 0; font-size: clamp(23px, 4vw, 31px); letter-spacing: -.035em; }
h3 { margin-bottom: 10px; }
.eyebrow { color: var(--accent); font-size: 11px; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; }
.lead { max-width: 690px; color: var(--muted); line-height: 1.55; }
.hero { position: relative; overflow: hidden; display: flex; justify-content: space-between; align-items: flex-end; gap: 18px; min-height: 210px; padding: clamp(24px, 6vw, 44px); border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line)); border-radius: 30px; background: var(--surface); }
.hero::after { content: ""; position: absolute; right: -80px; bottom: -120px; width: 220px; height: 220px; border: 34px solid color-mix(in srgb, var(--accent) 7%, transparent); border-radius: 50%; }
.hero h1 { margin: 5px 0 8px; }
.hero p { margin: 0; color: var(--muted); }
.hero-temp { z-index: 1; white-space: nowrap; font-size: clamp(48px, 12vw, 88px); font-weight: 250; letter-spacing: -.075em; }
.hero-temp small { margin-left: 4px; color: var(--muted); font-size: .28em; font-weight: 600; letter-spacing: 0; }
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0 34px; }
.metric { padding: 18px; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); }
.metric span { display: block; margin-bottom: 8px; color: var(--muted); font-size: 12px; }
.metric strong { font-size: 24px; letter-spacing: -.03em; }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 8px 0 18px; }
.device-grid, .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 14px; }
.device-card, .panel, .list-card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.device-card { overflow: hidden; padding: 16px; }
.device-card.off { opacity: .78; }
.device-card { overflow: hidden; padding: 20px; }
.device-card.off { opacity: .86; }
.device-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.device-title { min-width: 0; }
.device-title h3 { overflow: hidden; margin: 0 0 3px; color: var(--text); text-overflow: ellipsis; white-space: nowrap; font-size: 15px; }
.device-title p { overflow: hidden; margin: 0; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.device-title h3 { overflow: hidden; margin: 0 0 4px; text-overflow: ellipsis; white-space: nowrap; font-size: 19px; }
.device-title p { overflow: hidden; margin: 0; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.status { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
.status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--danger); }
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--danger); }
.status.online::before { background: var(--accent); }
.power-button {
display: grid;
place-items: center;
flex: 0 0 auto;
width: 36px;
height: 36px;
min-height: 36px;
padding: 0;
border-radius: var(--radius-sm);
color: var(--muted);
background: var(--surface-2);
font-size: 17px;
}
.power-button.on { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.temperature-control { display: flex; align-items: center; justify-content: center; gap: 16px; padding: 20px 0 14px; }
.temperature-control button {
display: grid;
place-items: center;
width: 36px;
height: 36px;
min-height: 36px;
padding: 0;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
color: var(--text-soft);
background: var(--surface);
font-size: 18px;
}
.target-temp { min-width: 100px; text-align: center; color: var(--text); font-size: 42px; font-weight: 320; line-height: 1; letter-spacing: -.055em; }
.target-temp small { margin-left: 3px; color: var(--muted); font-size: 13px; font-weight: 540; }
.current-line { display: flex; justify-content: center; gap: 6px; margin: -6px 0 15px; color: var(--muted); font-size: 11px; }
.current-line strong { color: var(--text-soft); font-weight: 560; }
.mode-row, .fan-row, .preset-row {
display: flex;
gap: 5px;
overflow-x: auto;
padding: 2px 0 8px;
scrollbar-width: none;
}
.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; }
.mode-row button, .fan-row button, .preset-row button {
flex: 1 0 auto;
min-width: 56px;
min-height: 32px;
padding: 6px 9px;
border-color: var(--line);
color: var(--muted);
background: var(--surface-2);
font-size: 11px;
font-weight: 540;
}
.mode-row button.active, .fan-row button.active, .preset-row button.active {
border-color: var(--accent-border);
color: var(--accent);
background: var(--accent-soft);
font-weight: 620;
}
.device-toggles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin-top: 2px; }
.device-toggles button { min-height: 32px; padding: 6px 5px; color: var(--muted); background: var(--surface-2); font-size: 10px; }
.device-toggles button.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.card-footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
.card-footer small { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.card-menu { display: flex; gap: 3px; }
.card-menu button { min-height: 28px; padding: 5px 7px; border-color: transparent; color: var(--muted); background: transparent; font-size: 10px; }
.card-menu button:hover { border-color: var(--line); color: var(--text); background: var(--surface-2); }
.list-card { padding: 15px; }
.power-button { display: grid; place-items: center; flex: 0 0 auto; width: 46px; height: 46px; padding: 0; border-radius: 50%; color: var(--muted); font-size: 21px; }
.power-button.on { background: var(--accent); color: var(--accent-text); }
.temperature-control { display: flex; align-items: center; justify-content: center; gap: 22px; padding: 24px 0 17px; }
.temperature-control button { width: 42px; height: 42px; padding: 0; border: 1px solid var(--line); border-radius: 50%; background: transparent; font-size: 22px; }
.target-temp { min-width: 106px; text-align: center; font-size: 48px; font-weight: 300; letter-spacing: -.06em; }
.target-temp small { margin-left: 3px; color: var(--muted); font-size: 15px; font-weight: 650; }
.current-line { display: flex; justify-content: center; gap: 6px; margin: -10px 0 18px; color: var(--muted); font-size: 12px; }
.current-line strong { color: var(--text); }
.mode-row, .fan-row { display: flex; gap: 7px; overflow-x: auto; padding: 2px 0 10px; scrollbar-width: none; }
.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar { display: none; }
.mode-row button, .fan-row button { flex: 1 0 auto; min-width: 58px; padding: 9px 10px; color: var(--muted); background: var(--surface-muted); font-size: 12px; }
.mode-row button.active, .fan-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; }
.device-toggles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 4px; }
.device-toggles button { padding: 9px 5px; color: var(--muted); font-size: 11px; }
.device-toggles button.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }
.card-footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.card-footer small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
.card-menu { display: flex; gap: 5px; }
.card-menu button { padding: 7px 9px; background: transparent; color: var(--muted); font-size: 12px; }
.list-card { padding: 19px; }
.list-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.list-card h3 { margin: 0 0 4px; color: var(--text); font-size: 15px; }
.list-card p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
.badge { display: inline-flex; align-items: center; min-height: 23px; padding: 3px 7px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); background: var(--surface-2); font-size: 10px; font-weight: 550; }
.badge.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.card-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin-top: 13px; }
.card-stat { padding: 8px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); text-align: center; }
.list-card h3 { margin: 0 0 5px; font-size: 18px; }
.list-card p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
.badge { display: inline-flex; align-items: center; padding: 5px 9px; border-radius: 99px; color: var(--muted); background: var(--surface-muted); font-size: 11px; }
.badge.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }
.card-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 16px; }
.card-stat { padding: 10px; border-radius: 12px; background: var(--surface-muted); text-align: center; }
.card-stat small, .card-stat strong { display: block; }
.card-stat small { margin-bottom: 4px; color: var(--muted); font-size: 9px; }
.card-stat strong { color: var(--text-soft); font-size: 13px; font-weight: 590; }
.empty { grid-column: 1/-1; padding: 34px 22px; border: 1px dashed var(--line-strong); border-radius: var(--radius); color: var(--muted); background: color-mix(in srgb, var(--surface) 50%, transparent); text-align: center; }
.empty strong { display: block; margin-bottom: 6px; color: var(--text); }
.empty.compact { padding: 16px; }
.panel { padding: 16px; }
.card-stat small { margin-bottom: 5px; color: var(--muted); font-size: 10px; }
.card-stat strong { font-size: 15px; }
.empty { grid-column: 1/-1; padding: 38px 24px; border: 1px dashed var(--line); border-radius: var(--radius); color: var(--muted); text-align: center; }
.empty strong { display: block; margin-bottom: 7px; color: var(--text); }
.panel { padding: 20px; }
.chart-panel { overflow: hidden; }
.history-chart-card { margin-top: 12px; }
.chart-toolbar { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 14px; }
.chart-toolbar select { flex: 1; min-width: 160px; }
.chart-title-row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:10px; }
.chart-title-row h3 { margin:0 0 4px; font-size:14px; }
.chart-title-row p { margin:0; color:var(--muted); font-size:11px; }
.history-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; }
.history-stat { min-width:0; padding:10px; border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--surface-2); }
.history-stat small,.history-stat strong,.history-stat span { display:block; }
.history-stat small { color:var(--muted); font-size:10px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.history-stat strong { margin-top:4px; color:var(--text); font-size:17px; font-weight:620; }
.history-stat span { margin-top:2px; color:var(--muted-2); font-size:9px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.chart-wrap { position: relative; overflow-x: auto; min-height: 310px; scrollbar-width:none; -ms-overflow-style:none; }
.chart-wrap::-webkit-scrollbar { display:none; }
.chart-wrap.compact-chart { min-height:230px; }
#historyTemperatureChart,#historyOperationChart { width:100%; min-width:720px; }
#historyTemperatureChart { height:360px; }
#historyOperationChart { height:260px; }
.legend { display:flex; flex-wrap:wrap; gap:8px 16px; margin-top:9px; color:var(--muted); font-size:10px; }
.legend > span { display:flex; align-items:center; gap:6px; }
.legend-line { width:16px; height:2px; border-radius:999px; background:var(--legend-color,var(--accent)); }
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.chart-toolbar { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 18px; }
.chart-toolbar select { flex: 1; min-width: 140px; }
.chart-wrap { position: relative; overflow-x: auto; min-height: 310px; }
#historyChart { width: 100%; min-width: 680px; height: 340px; }
.legend { display: flex; gap: 18px; margin-top: 10px; color: var(--muted); font-size: 12px; }
.legend > span { display: flex; align-items: center; 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 h3 { margin: 2px 0 0; }
.form-grid hr, .dialog-form hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid var(--line); }
label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 550; }
input, select, textarea {
width: 100%;
min-height: var(--control-h);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
outline: 0;
padding: 8px 10px;
color: var(--text);
background: var(--input-bg);
font-size: 13px;
transition: border-color .12s ease, background-color .12s ease;
}
input:hover, select:hover, textarea:hover { border-color: var(--line-strong); }
input:focus, select:focus, textarea:focus { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); }
input::placeholder, textarea::placeholder { color: var(--muted-2); }
.check { display: flex; grid-auto-flow: column; justify-content: start; align-items: center; gap: 8px; min-height: 36px; }
.check input { width: 16px; min-height: 16px; accent-color: var(--accent); }
.form-actions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 4px; }
.system-panel { margin-top: 10px; color: var(--muted); font-size: 12px; line-height: 1.7; }
.system-panel strong { color: var(--text-soft); font-weight: 580; }
.log-list { display: grid; gap: 0; padding: 0; overflow: hidden; }
.log-row { display: grid; grid-template-columns: 78px 150px 1fr; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--line); font-size: 11px; }
.form-grid hr, .dialog-form hr { width: 100%; border: 0; border-top: 1px solid var(--line); }
label { display: grid; gap: 7px; color: var(--muted); font-size: 12px; font-weight: 650; }
input, select { width: 100%; min-height: 45px; border: 1px solid var(--line); border-radius: 12px; outline: 0; padding: 10px 12px; color: var(--text); background: var(--input-bg); }
input:focus, select:focus { border-color: var(--accent); outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent); outline-offset: 1px; }
.check { display: flex; grid-auto-flow: column; justify-content: start; align-items: center; gap: 9px; min-height: 44px; }
.check input { width: 19px; min-height: 19px; accent-color: var(--accent); }
.form-actions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 6px; }
.system-panel { margin-top: 14px; color: var(--muted); line-height: 1.7; }
.system-panel strong { color: var(--text); }
.log-list { display: grid; gap: 2px; padding: 8px; }
.log-row { display: grid; grid-template-columns: 78px 150px 1fr; gap: 12px; padding: 11px 12px; border-bottom: 1px solid var(--line); font-size: 12px; }
.log-row:last-child { border-bottom: 0; }
.log-row:hover { background: var(--surface-2); }
.log-row time, .log-row .kind { color: var(--muted); }
.log-row.error .kind { color: var(--danger); }
.log-row.warn .kind { color: var(--warning); }
.bottom-nav {
position: fixed;
z-index: 30;
right: 0;
bottom: 0;
left: 0;
display: grid;
grid-template-columns: repeat(5, 1fr);
padding: 6px max(8px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
border-top: 1px solid var(--line);
background: var(--nav-bg);
}
.bottom-nav button {
display: grid;
place-items: center;
gap: 3px;
min-height: 50px;
padding: 5px 2px;
border-color: transparent;
border-radius: var(--radius-sm);
color: var(--muted);
background: transparent;
font-size: 10px;
}
.bottom-nav button:hover { border-color: transparent; color: var(--text); background: var(--surface-2); }
.bottom-nav button span { display: grid; place-items: center; width: 20px; height: 20px; color: currentColor; font-size: 16px; line-height: 1; }
.bottom-nav button span svg { width: 18px; height: 18px; stroke-width: 1.6; }
.bottom-nav button b { font-weight: 540; }
.bottom-nav button.active { color: var(--accent); background: var(--accent-soft); }
.desktop-nav-only { display: none !important; }
/* Dialogs deliberately have no drop shadow: separation comes from border + backdrop. */
dialog {
width: min(540px, calc(100% - 24px));
max-height: min(88vh, 760px);
overflow: auto;
border: 1px solid var(--line-strong);
border-radius: 10px;
padding: 0;
color: var(--text);
background: var(--dialog-bg);
}
dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
.dialog-form { display: grid; gap: 12px; padding: 17px; }
.bottom-nav { position: fixed; z-index: 30; right: 0; bottom: 0; left: 0; display: grid; grid-template-columns: repeat(5, 1fr); padding: 8px max(8px, env(safe-area-inset-right)) calc(8px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); border-top: 1px solid var(--line); background: color-mix(in srgb, var(--nav-bg) 96%, transparent); backdrop-filter: blur(14px); }
.bottom-nav button { display: grid; place-items: center; gap: 3px; padding: 7px 2px; border-radius: 12px; color: var(--muted); background: transparent; font-size: 10px; }
.bottom-nav button span { font-size: 20px; line-height: 1; }
.bottom-nav button b { font-weight: 650; }
.bottom-nav button.active { color: var(--accent); }
dialog { width: min(540px, calc(100% - 24px)); max-height: min(88vh, 760px); overflow: auto; border: 1px solid var(--line); border-radius: 24px; padding: 0; color: var(--text); background: var(--dialog-bg); }
dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(4px); }
.dialog-form { display: grid; gap: 14px; padding: 20px; }
.dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.dialog-head h2 { margin: 0; font-size: 18px; }
.dialog-head button { display: grid; place-items: center; width: 32px; min-width: 32px; height: 32px; min-height: 32px; padding: 0; border-color: transparent; color: var(--muted); background: transparent; font-size: 21px; }
.dialog-head button:hover { border-color: var(--line); color: var(--text); background: var(--surface-2); }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
#externalSensorFields { display: grid; gap: 10px; }
.field-note { margin: -3px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.sensor-detail { margin-top: 9px; color: var(--muted); font-size: 11px; line-height: 1.5; }
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 9px; }
legend { padding: 0 5px; color: var(--muted); font-size: 10px; }
.dialog-head h2 { font-size: 23px; }
.dialog-head button { width: 38px; height: 38px; padding: 0; background: transparent; color: var(--muted); font-size: 25px; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
#externalSensorFields { display: grid; gap: 12px; }
.field-note { margin: -4px 0 0; color: var(--muted); font-size: .8rem; line-height: 1.45; }
.sensor-detail { margin-top: 10px; color: var(--muted); font-size: .8rem; line-height: 1.45; }
fieldset { border: 1px solid var(--line); border-radius: 14px; padding: 10px; }
legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.days label { display: grid; place-items: center; gap: 4px; padding: 6px 2px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); font-size: 9px; }
.days input { width: 14px; min-height: 14px; accent-color: var(--accent); }
.sheet { width: min(500px, calc(100% - 18px)); margin: auto auto 8px; }
.sheet .dialog-head { padding: 15px 15px 4px; }
.menu-list { display: grid; padding: 7px 9px 10px; }
.menu-list button { display: flex; justify-content: space-between; min-height: 42px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; padding: 11px 8px; background: transparent; text-align: left; }
.menu-list button:hover { background: var(--surface-2); }
.days label { display: grid; place-items: center; gap: 5px; padding: 6px 2px; border-radius: 8px; background: var(--surface-muted); font-size: 10px; }
.days input { width: 16px; min-height: 16px; accent-color: var(--accent); }
.sheet { width: min(500px, calc(100% - 18px)); margin: auto auto 10px; }
.sheet .dialog-head { padding: 18px 18px 5px; }
.menu-list { display: grid; padding: 8px 10px 12px; }
.menu-list button { display: flex; justify-content: space-between; border-bottom: 1px solid var(--line); border-radius: 0; padding: 16px 9px; background: transparent; text-align: left; }
.menu-list button:last-child { border-bottom: 0; }
.auth-dialog { width: min(420px, calc(100% - 30px)); }
#toast {
position: fixed;
z-index: 100;
right: 16px;
bottom: 82px;
max-width: min(380px, calc(100% - 32px));
transform: translateY(8px);
opacity: 0;
pointer-events: none;
padding: 10px 12px;
border: 1px solid var(--line-strong);
border-radius: var(--radius);
color: var(--text-soft);
background: var(--surface-2);
transition: .14s ease;
font-size: 12px;
}
#toast { position: fixed; z-index: 100; right: 18px; bottom: 92px; max-width: min(380px, calc(100% - 36px)); transform: translateY(15px); opacity: 0; pointer-events: none; padding: 12px 15px; border: 1px solid var(--line); border-radius: 13px; background: var(--surface-2); transition: .2s ease; font-size: 13px; }
#toast.show { transform: translateY(0); opacity: 1; }
#toast.error { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, var(--line)); background: color-mix(in srgb, var(--danger-soft) 42%, var(--surface-2)); }
.token-manager { display: grid; gap: 10px; }
.token-list { display: grid; gap: 6px; }
.token-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); }
.token-row > div { min-width: 0; display: grid; gap: 2px; }
.token-row strong { color: var(--text); font-size: 12px; font-weight: 590; }
.token-row small { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
/* Zone quick controls */
.zone-quick-control { display: grid; gap: 4px; margin-top: 9px; padding: 8px 9px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); }
.temperature-control.compact { padding: 3px 0 6px; gap: 12px; }
.temperature-control.compact button { width: 34px; height: 34px; min-height: 34px; }
.target-temp.compact { min-width: 86px; font-size: 29px; }
.zone-quick-control .mode-row { padding-bottom: 1px; }
.discovery-name-list { display: grid; gap: 10px; }
.discovery-name-row { display: grid; gap: 7px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.discovery-name-row:last-child { border-bottom: 0; }
.discovery-name-row span { display: grid; gap: 2px; }
.discovery-name-row small { color: var(--muted); overflow-wrap: anywhere; }
/* Quiet themed scrollbars. No bright native tracks in dark mode. */
* { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: 999px; background: var(--scroll-thumb); background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: padding-box; }
.house-climate { display: grid; gap: 13px; margin: -12px 0 28px; }
.house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.house-climate-head h3 { margin: 2px 0 3px; font-size: 15px; }
.house-climate-head p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.outside-pill { flex: 0 0 auto; min-width: 90px; padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); text-align: right; }
.outside-pill small, .outside-pill strong { display: block; }
.outside-pill small { color: var(--muted); font-size: 9px; }
.outside-pill strong { margin-top: 2px; color: var(--text); font-size: 16px; font-weight: 590; }
.house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.house-mode-row button { min-height: 36px; color: var(--muted); background: var(--surface-2); }
.house-mode-row button.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); font-weight: 620; }
.mode-indicator { display: inline-block; width: 6px; height: 6px; margin-right: 6px; border-radius: 999px; vertical-align: 1px; background: var(--muted-2); }
.house-mode-row button[data-value="cool"] .mode-indicator { background: #60a5fa; }
.house-mode-row button[data-value="heat"] .mode-indicator { background: var(--warning); }
.house-mode-row button.active .mode-indicator { box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 9%, transparent); }
.zone-thermostat { overflow: hidden; }
.zone-thermostat.demanding { border-color: var(--accent-border); }
.thermostat-main { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; margin: 13px 0 8px; }
.thermostat-main > div:first-child { text-align: left; }
.thermostat-main > div:last-child { text-align: right; }
.thermostat-main small, .thermostat-main strong { display: block; }
.thermostat-main small { margin-bottom: 3px; color: var(--muted); font-size: 9px; }
.thermostat-main strong { color: var(--text-soft); font-size: 14px; font-weight: 590; }
.zone-mode-row { padding-bottom: 4px; }
.zone-state-line { display: flex; justify-content: space-between; gap: 10px; padding: 7px 1px 0; color: var(--muted); font-size: 10px; }
.zone-state-line span:last-child { text-align: right; }
.dashboard-zones { margin-bottom: 28px; }
.schedule-template-panel { display: grid; gap: 11px; margin-bottom: 14px; }
.schedule-template-panel h3 { margin-bottom: 3px; }
.template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; }
.profile-editor { border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 9px 10px; background: var(--surface-2); }
.profile-editor summary { cursor: pointer; color: var(--text-soft); font-weight: 590; font-size: 12px; }
.profile-editor[open] summary { margin-bottom: 10px; }
.profile-grid { display: grid; grid-template-columns: 90px repeat(3, 1fr); align-items: end; gap: 7px; margin-top: 7px; }
.profile-grid > strong { align-self: center; font-size: 11px; }
.profile-grid label span { font-size: 9px; }
.warning-note { color: var(--warning); }
.automation-hint { margin-bottom: 14px; }
.automation-hint strong { display: block; margin-bottom: 4px; color: var(--text-soft); font-size: 12px; }
.automation-hint p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.55; }
#toast.error { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, var(--line)); }
@media (min-width: 900px) {
.topbar { padding-left: 14px; }
.brand { width: 176px; flex: 0 0 176px; }
.bottom-nav {
top: 54px;
right: auto;
bottom: 0;
left: 0;
align-content: start;
grid-template-columns: 1fr;
width: 190px;
padding: 10px 8px;
border-top: 0;
border-right: 1px solid var(--line);
background: var(--nav-bg);
}
.bottom-nav button {
grid-template-columns: 22px 1fr;
justify-items: start;
place-items: center start;
gap: 8px;
min-height: 38px;
padding: 7px 9px;
text-align: left;
font-size: 12px;
}
.bottom-nav button span { width: 18px; height: 18px; }
.bottom-nav button b { font-size: 12px; }
.bottom-nav button.active { color: var(--text); background: var(--surface-2); }
.bottom-nav button.active span { color: var(--accent); }
.bottom-nav .desktop-nav-only { display: grid !important; }
.bottom-nav button[data-nav="more"] { display: none; }
main {
width: min(1360px, calc(100% - 190px));
margin: 0 0 0 190px;
padding: 30px 28px 56px;
}
#toast { bottom: 20px; }
.bottom-nav { top: 92px; right: auto; bottom: auto; left: max(16px, calc((100vw - 1380px)/2)); grid-template-columns: 1fr; width: 90px; border: 1px solid var(--line); border-radius: 22px; padding: 9px; background: var(--surface); }
.bottom-nav button { padding: 10px 4px; }
main { padding-left: 122px; padding-bottom: 50px; }
}
@media (max-width: 720px) {
.topbar { flex-wrap: wrap; gap: 6px; padding: 8px 10px; }
.topbar { flex-wrap: wrap; }
.top-actions { width: 100%; }
.top-actions .primary { margin-left: auto; }
}
@media (max-width: 620px) {
main { padding: 15px 11px 92px; }
.hero { min-height: 142px; padding: 18px; }
.hero-temp { font-size: 48px; }
.metrics { gap: 6px; }
.metric { padding: 11px 9px; }
.metric strong { font-size: 18px; }
main { padding: 18px 13px 105px; }
.hero { min-height: 190px; padding: 23px; }
.hero-temp { font-size: 52px; }
.metrics { gap: 7px; }
.metric { padding: 14px 10px; }
.metric strong { font-size: 20px; }
.section-heading { align-items: center; }
.section-heading button { min-height: 32px; padding: 6px 9px; font-size: 11px; }
.section-heading button { padding: 9px 11px; 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; }
.log-row .message { grid-column: 1/-1; }
.theme-select { min-width: 84px; }
.theme-select { min-width: 86px; }
}
@media (max-width: 410px) {
.topbar { padding-inline: 12px; }
.brand strong { font-size: 13px; }
.hero { align-items: start; flex-direction: column; }
.hero-temp { align-self: flex-end; margin-top: -15px; }
.two { grid-template-columns: 1fr; }
.toolbar-select { min-width: 54px; }
.theme-select { min-width: 82px; }
}
.token-manager { display: grid; gap: 12px; }
.token-list { display: grid; gap: 8px; }
.token-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-muted); }
.token-row > div { min-width: 0; display: grid; gap: 3px; }
.token-row strong { color: var(--text); font-size: 13px; }
.token-row small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
.empty.compact { padding: 18px; }
@media (max-width: 620px) {
.token-row { align-items: stretch; flex-direction: column; }
.token-row button { width: 100%; }
.house-climate { margin-top: -18px; }
}
/* 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; }
.temperature-control.compact button { width: 38px; height: 38px; }
.target-temp.compact { min-width: 92px; font-size: 32px; }
.zone-quick-control .mode-row { padding-bottom: 2px; }
.discovery-name-list { display:grid; gap:12px; }
.discovery-name-row { display:grid; gap:8px; padding:12px 0; border-bottom:1px solid var(--border); }
.discovery-name-row:last-child { border-bottom:0; }
.discovery-name-row span { display:grid; gap:2px; }
.discovery-name-row small { color:var(--muted); overflow-wrap:anywhere; }
/* Themed scrollbars: visible where scrolling is useful, but visually quiet. */
* { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted); }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: var(--surface-muted); border-radius: 99px; }
*::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--accent) 38%, var(--surface-2)); border: 2px solid var(--surface-muted); border-radius: 99px; }
*::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--accent) 58%, var(--surface-2)); }
.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 34px; }
.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; }
.outside-pill { flex: 0 0 auto; min-width: 94px; padding: 10px 13px; border-radius: 14px; background: var(--surface-muted); text-align: right; }
.outside-pill small, .outside-pill strong { display: block; }
.outside-pill small { color: var(--muted); font-size: 10px; }
.outside-pill strong { margin-top: 3px; font-size: 18px; }
.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-mode-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; }
.zone-thermostat { overflow: hidden; }
.zone-thermostat.demanding { border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); }
.thermostat-main { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; margin: 15px 0 9px; }
.thermostat-main > div:first-child { text-align: left; }
.thermostat-main > div:last-child { text-align: right; }
.thermostat-main small, .thermostat-main strong { display: block; }
.thermostat-main small { margin-bottom: 3px; color: var(--muted); font-size: 10px; }
.thermostat-main strong { font-size: 16px; }
.preset-row { display: flex; gap: 6px; overflow-x: auto; padding: 3px 0 8px; }
.preset-row button { flex: 1 0 auto; min-height: 38px; padding: 8px 11px; color: var(--muted); background: var(--surface-muted); font-size: 12px; }
.preset-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; }
.zone-mode-row { padding-bottom: 5px; }
.zone-state-line { display: flex; justify-content: space-between; gap: 10px; padding: 8px 1px 1px; color: var(--muted); font-size: 11px; }
.zone-state-line span:last-child { text-align: right; }
.dashboard-zones { margin-bottom: 34px; }
.schedule-template-panel { display: grid; gap: 13px; margin-bottom: 18px; }
.schedule-template-panel h3 { margin-bottom: 4px; }
.template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
.profile-editor { border: 1px solid var(--line); border-radius: 14px; padding: 11px 12px; background: var(--surface-muted); }
.profile-editor summary { cursor: pointer; color: var(--text); font-weight: 700; font-size: 13px; }
.profile-editor[open] summary { margin-bottom: 12px; }
.profile-grid { display: grid; grid-template-columns: 90px repeat(3, 1fr); align-items: end; gap: 8px; margin-top: 8px; }
.profile-grid > strong { align-self: center; font-size: 12px; }
.profile-grid label span { font-size: 10px; }
.warning-note { color: var(--warning); }
@media (max-width: 620px) {
.house-climate { margin-top: -20px; }
.house-climate-head { align-items: flex-start; }
.house-climate-head p { max-width: 230px; }
.house-climate-head p { max-width: 220px; }
.thermostat-main { grid-template-columns: 1fr auto 1fr; gap: 4px; }
.thermostat-main .temperature-control.compact { gap: 7px; }
.thermostat-main .target-temp.compact { min-width: 70px; font-size: 26px; }
.thermostat-main .temperature-control.compact { gap: 9px; }
.thermostat-main .target-temp.compact { min-width: 75px; font-size: 28px; }
.profile-grid { grid-template-columns: 1fr 1fr; }
.profile-grid > strong { grid-column: 1/-1; margin-top: 3px; }
.profile-grid > strong { grid-column: 1/-1; margin-top: 4px; }
.zone-state-line { flex-direction: column; gap: 2px; }
.zone-state-line span:last-child { text-align: left; }
}
@media (max-width: 410px) {
.brand strong { font-size: 12px; }
.hero { align-items: flex-start; flex-direction: column; }
.hero-temp { align-self: flex-end; margin-top: -12px; }
.two { grid-template-columns: 1fr; }
.toolbar-select { min-width: 52px; }
.theme-select { min-width: 78px; }
}
.automation-hint { margin-bottom: 18px; }
.automation-hint strong { display: block; margin-bottom: 5px; }
.automation-hint p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.5; }
@media (max-width: 720px) {
.history-summary { grid-template-columns:repeat(2,minmax(0,1fr)); }
.chart-toolbar { display:grid; grid-template-columns:1fr 1fr; }
.chart-toolbar button { grid-column:1/-1; }
/* Rich climate history (v0.4.2+) using the classic rounded UI. */
.history-chart-card { margin-top: 14px; }
.chart-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.chart-title-row h3 { margin: 0 0 4px; font-size: 17px; }
.chart-title-row p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.45; }
.history-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; }
.history-stat { min-width: 0; padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-muted); }
.history-stat small, .history-stat strong, .history-stat span { display: block; }
.history-stat small { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.history-stat strong { margin-top: 4px; color: var(--text); font-size: 18px; font-weight: 700; }
.history-stat span { overflow: hidden; margin-top: 2px; color: var(--muted-2); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.chart-wrap { scrollbar-width: none; -ms-overflow-style: none; }
.chart-wrap::-webkit-scrollbar { display: none; }
.chart-wrap.compact-chart { min-height: 230px; }
#historyTemperatureChart, #historyOperationChart { width: 100%; min-width: 720px; }
#historyTemperatureChart { height: 360px; }
#historyOperationChart { height: 260px; }
.legend { flex-wrap: wrap; gap: 9px 18px; }
.legend-line { width: 18px; height: 3px; border-radius: 999px; background: var(--legend-color, var(--accent)); }
@media (max-width: 620px) {
.history-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.chart-toolbar { display: grid; grid-template-columns: 1fr 1fr; }
.chart-toolbar button { grid-column: 1 / -1; }
}
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v042';
const CACHE = 'gree-controller-v043';
const ASSETS = ['/', '/styles.css', '/app.js', '/favicon.svg', '/manifest.webmanifest', '/lang/index.json', '/lang/en.json'];
self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting())));
self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim())));