diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md
index 3fc71af..1ba9678 100644
--- a/BUILD_REPORT.md
+++ b/BUILD_REPORT.md
@@ -1,4 +1,4 @@
-# GREE Controller v0.6.8 - build and validation report
+# GREE Controller v0.6.9 - build and validation report
## Scope
@@ -180,3 +180,14 @@ Validation performed in this packaging environment:
- Kept Control plan, Zones and Devices as collapsed disclosure panels.
- Reduced dashboard hero, metric, global-control, group-card and disclosure spacing for a denser layout without removing functionality.
- Advanced PWA cache key to `gree-controller-v068-dashboard-wrap`.
+
+
+## 0.6.9 dashboard fit, compact summary and group-aware control plan
+
+- Reworked dashboard Quick Thermostats so the target-temperature control occupies its own row and measured/device-target temperatures sit below it; labels no longer collide or clip in medium-width cards.
+- Kept the quick-thermostat list as a wrapped responsive grid with no horizontal scrolling.
+- Compressed the Home / Comfort under control summary into one short card; average temperature and small online/active/demand counters now share the same row instead of consuming a separate large hero plus metrics row.
+- Reduced dashboard control-plan blocks to compact simulator-style cards.
+- Added explicit climate-group cards to What happens next, combining member demand and upcoming member-zone events.
+- Zone plan cards identify their climate group membership, and automation plan rows preserve group targets.
+- Advanced the PWA cache key to `gree-controller-v069-dashboard-fit`.
diff --git a/Cargo.lock b/Cargo.lock
index 9c127cd..69923ab 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
-version = "0.6.8"
+version = "0.6.9"
dependencies = [
"aes",
"aes-gcm",
diff --git a/Cargo.toml b/Cargo.toml
index 9d4374f..e0b7cb0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
-version = "0.6.8"
+version = "0.6.9"
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 79b54ea..6fb1ca9 100644
--- a/FILE_MANIFEST.sha256
+++ b/FILE_MANIFEST.sha256
@@ -1,10 +1,10 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
-4c31bf357041b674d29a7ab5751b6060ad8650178e227aa3137d9195a49a2c03 ./BUILD_REPORT.md
-006a65924eb99c5c911edeea96f6f1498f44734eaf638af81d9562f7e41faaf2 ./Cargo.lock
-b0dfc13a0710b9f4cc0d2417129335ef277509ce951bcf85781bc38cf8aa892e ./Cargo.toml
+e6eb0b2882aa446546fc5a4e63fb11973e79483c263fa053fc10cb2e9aaf53bb ./BUILD_REPORT.md
+9227483cd15d1d93b01b312d119179c8348cffda4bd4870571cdabfa8e972b14 ./Cargo.lock
+b8fcf5528e25c4823ca90b2565dd025994aafadfa35360af475db66ef53d2359 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
-1dbfc1eab7344a021cdce4859b5ae03faf30974d3b9c7c46d49e933bb34cdfdd ./README.md
+191d7fd1242937ba1c3df7f0b3014c56a6944f6445cd1453832b4dc8024d37bf ./README.md
f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs
74fc74d2bb05541f2137250c6a73a9f39a6fbd838e5d95d8c8efd7ebefc384f0 ./docs/API.md
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
@@ -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
-3019758a902fbd38b1faf7d25ed2bea8e4ab124dca34cc6fe0d05a9d4e5f3714 ./home-assistant/custom_components/gree_controller/manifest.json
+97117268892b7868ff4001017d422c30a94b68f4c656cbb8c5fa1d65c23fd65f ./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
-3de7cd88c5c879ba797e805962afa7d449fbd1a016ca8ecf4740d2348ebd39f7 ./lang/en.json
-1a23e91ed6bec4850cd1304dad9669041b08ea75114f3de3ececd5d5eaeee258 ./lang/pl.json
+6385512c4d23d53abca0c1e178420733baf30232a20d00e53688c7b8da91ac01 ./lang/en.json
+067888c4d220169604303f3236e4832e2935bf2a20633cbff7fa86ec8dc9785d ./lang/pl.json
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
@@ -62,10 +62,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
66d7a1a12843975506ffef2128d161627b96d665d39cedde2d791651e4598ee1 ./src/queries.rs
f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
-d1971287c12f5d022e0607dca6ec065d11903432fa6ec0df5584cb5ad0a524e2 ./web/app.js
+dfb89acd9b040c32b24146198d66ca98a0bc3dfbc852ac03b2ca3d252923ec3e ./web/app.js
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
-e665efcc1fac4138d6ce83309e5c6766e80901292bd25983d35613ff3c754b9d ./web/index.html
+f3b864558d327d769a6d4a33e88b96a518ec900ce39d6af990bd9cabb3b075a5 ./web/index.html
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
-0e9e57efd2ba706c315c34d43c95930455fa3ce720bfac6a506b89a755fc4349 ./web/styles.css
-9099a3d1d4b2775241728b22fa81cd07ecebc34cb160a729feeca7af3efcdf03 ./web/sw.js
+77c96fe9ca242edb44defe2f7df40b99e7ec2c546d8d13c1c31cc55beea6522c ./web/styles.css
+17f83bd268aac0a48e0ad230b970ca868df330f8884cc03747d8bee9aa8aef5d ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
diff --git a/README.md b/README.md
index 470a85d..77e07c3 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.8**.
+Current version: **0.6.9**.
## Highlights
@@ -47,7 +47,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.8.zip
+unzip gree-controller-v0.6.9.zip
cd gree-controller
chmod +x scripts/*.sh
./scripts/dev.sh
diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json
index 9ac15ad..0b8f09c 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.8",
+ "version": "0.6.9",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
diff --git a/lang/en.json b/lang/en.json
index 9f03a7f..a4c34da 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -362,6 +362,8 @@
"plan.rules": "Extra automations",
"plan.ruleCount": "{count} active rules",
"plan.event": "Change",
+ "plan.groupSummary": "Group zones: {zones} · requesting action: {demand}",
+ "plan.groupOff": "Group is turned off",
"zones.enable": "Enable zone",
"zones.disable": "Disable zone",
"history.90d": "90 days",
diff --git a/lang/pl.json b/lang/pl.json
index 2441624..c520a9a 100644
--- a/lang/pl.json
+++ b/lang/pl.json
@@ -362,6 +362,8 @@
"plan.rules": "Dodatkowe automatyzacje",
"plan.ruleCount": "Aktywne reguły: {count}",
"plan.event": "Zmiana",
+ "plan.groupSummary": "Strefy w grupie: {zones} · żądające działania: {demand}",
+ "plan.groupOff": "Grupa jest wyłączona",
"zones.enable": "Włącz strefę",
"zones.disable": "Wyłącz strefę",
"history.90d": "90 dni",
diff --git a/web/app.js b/web/app.js
index 7ff60a7..cb26d1a 100644
--- a/web/app.js
+++ b/web/app.js
@@ -285,18 +285,42 @@ function renderControlPlan() {
host.innerHTML = `
${esc(tr('plan.loading'))}
`;
return;
}
- const houseEvents = (plan.next_events || []).slice(0, 5);
- const house = `${esc(tr('plan.house'))}
${esc(houseModeLabel(plan.house_mode || 'off'))}
${esc(plan.control_strategy || 'setpoint')}${esc(tr('plan.houseSummary', {zones:(plan.zones || []).filter(zone=>zone.enabled).length, demand:(plan.zones || []).filter(zone=>zone.enabled && zone.demand).length}))}
${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `- ${esc(tr('plan.noEvents'))}
`}
`;
- const zones = (plan.zones || []).map(zone => {
- const events = (zone.next_events || []).slice(0, 3);
- const target = zone.target_temperature == null ? '--' : Number(zone.target_temperature).toFixed(1);
- return `${esc(zone.device_name || tr('common.noDevice'))}
${esc(zone.zone_name)}
${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}${fmtTemp(zone.current_temperature)}→${esc(target)}°C
${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}
${events.length ? events.map(planEventMarkup).join('') : `- ${esc(tr('plan.noEvents'))}
`}
`;
- }).join('');
- const rules = (plan.rules || []).filter(rule => rule.enabled);
- const ruleCard = rules.length ? `${esc(tr('plan.rules'))}
${esc(tr('plan.ruleCount', {count:rules.length}))}
${rules.slice(0,5).map(rule=>`- ${esc(rule.name)} → ${esc(rule.action_device_name || '')}
`).join('')}
` : '';
- host.innerHTML = house + zones + ruleCard;
-}
+ const allZones = plan.zones || [];
+ const houseEvents = (plan.next_events || []).slice(0, 3);
+ const house = `${esc(tr('plan.house'))}
${esc(houseModeLabel(plan.house_mode || 'off'))}
${esc(plan.control_strategy || 'setpoint')}${esc(tr('plan.houseSummary', {zones:allZones.filter(zone=>zone.enabled).length, demand:allZones.filter(zone=>zone.enabled && zone.demand).length}))}
${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `- ${esc(tr('plan.noEvents'))}
`}
`;
+
+ const groupCards = (app.groups || []).map(group => {
+ const memberIds = new Set(group.zone_ids || []);
+ const members = allZones.filter(zone => memberIds.has(zone.zone_id));
+ const state = groupState(group);
+ const powerEnabled = group.power_enabled !== false;
+ const demand = members.filter(zone => zone.enabled && zone.demand).length;
+ const mode = state.mode === 'house' ? tr('groups.followHouse') : state.mode === 'mixed' ? tr('groups.mixed') : modeLabel(state.mode);
+ const preset = state.preset === 'mixed' ? tr('groups.mixed') : zonePresetLabel(state.preset);
+ const events = members.flatMap(zone => (zone.next_events || []).map(event => ({...event, label:`${zone.zone_name}: ${event.label}`})))
+ .sort((a,b) => new Date(a.at) - new Date(b.at)).slice(0, 2);
+ return `${esc(tr('groups.group'))}
${esc(group.name)}
${esc(powerEnabled ? tr('common.on') : tr('common.off'))}${esc(mode)}·${esc(preset)}
${esc(tr('plan.groupSummary', {zones:members.length, demand}))}
${events.length ? events.map(planEventMarkup).join('') : `- ${esc(tr(powerEnabled ? 'plan.noEvents' : 'plan.groupOff'))}
`}
`;
+ }).join('');
+
+ const zoneGroups = new Map();
+ (app.groups || []).forEach(group => (group.zone_ids || []).forEach(zoneId => {
+ const names = zoneGroups.get(zoneId) || [];
+ names.push(group.name);
+ zoneGroups.set(zoneId, names);
+ }));
+ const zones = allZones.map(zone => {
+ const events = (zone.next_events || []).slice(0, 2);
+ const target = zone.target_temperature == null ? '--' : Number(zone.target_temperature).toFixed(1);
+ const groupNames = zoneGroups.get(zone.zone_id) || [];
+ const scope = groupNames.length ? `${tr('groups.group')}: ${groupNames.join(' · ')}` : (zone.device_name || tr('common.noDevice'));
+ return `${esc(scope)}
${esc(zone.zone_name)}
${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}${fmtTemp(zone.current_temperature)}→${esc(target)}°C
${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}
${events.length ? events.map(planEventMarkup).join('') : `- ${esc(tr('plan.noEvents'))}
`}
`;
+ }).join('');
+
+ const rules = (plan.rules || []).filter(rule => rule.enabled);
+ const ruleCard = rules.length ? `${esc(tr('plan.rules'))}
${esc(tr('plan.ruleCount', {count:rules.length}))}
${rules.slice(0,3).map(rule=>`- ${esc(rule.name)} → ${esc(rule.action_group_name ? `${tr('groups.group')}: ${rule.action_group_name}` : (rule.action_device_name || ''))}
`).join('')}
` : '';
+ host.innerHTML = house + groupCards + zones + ruleCard;
+}
async function loadControlPlan() {
try { app.controlPlan = await api('/api/control-plan'); renderControlPlan(); renderSimulationPage(); }
catch (error) { console.warn('Unable to load control plan:', error); }
diff --git a/web/index.html b/web/index.html
index 4b99a4f..be9df60 100644
--- a/web/index.html
+++ b/web/index.html
@@ -32,11 +32,10 @@
-
-
HomeComfort under control
Loading devices…
-
--°C
+
+
HomeComfort under control
Loading devices…
+
-
Global powerAll air conditioners
Master power for all enabled units. Whole-house mode or profile selection automatically switches it back on.
diff --git a/web/styles.css b/web/styles.css
index f1d9d0b..9bf1deb 100644
--- a/web/styles.css
+++ b/web/styles.css
@@ -762,3 +762,82 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
.dashboard-disclosure > summary { padding:12px 13px; }
.dashboard-disclosure-body { padding:10px; }
}
+
+/* 0.6.9 dashboard density and fit fixes */
+[data-view="dashboard"] .dashboard-summary-card {
+ min-height:0;
+ align-items:center;
+ gap:14px;
+ padding:13px 16px;
+ border-radius:18px;
+}
+[data-view="dashboard"] .dashboard-summary-card::after { width:100px; height:100px; right:-44px; bottom:-64px; border-width:18px; }
+[data-view="dashboard"] .dashboard-summary-copy { min-width:0; }
+[data-view="dashboard"] .dashboard-summary-copy .eyebrow { font-size:9px; }
+[data-view="dashboard"] .dashboard-summary-copy h1 { margin:2px 0 3px; font-size:20px; letter-spacing:-.025em; }
+[data-view="dashboard"] .dashboard-summary-copy p { font-size:11px; line-height:1.3; }
+[data-view="dashboard"] .dashboard-summary-status { z-index:1; display:flex; align-items:center; justify-content:flex-end; min-width:0; gap:10px; }
+[data-view="dashboard"] .dashboard-summary-status .metrics { display:flex; align-items:center; gap:5px; margin:0; }
+[data-view="dashboard"] .dashboard-summary-status .metric { display:flex; align-items:baseline; gap:5px; min-width:0; padding:6px 8px; border-radius:11px; background:var(--surface-muted); }
+[data-view="dashboard"] .dashboard-summary-status .metric span { margin:0; font-size:9px; white-space:nowrap; }
+[data-view="dashboard"] .dashboard-summary-status .metric strong { font-size:13px; line-height:1; }
+[data-view="dashboard"] .dashboard-summary-status .hero-temp { min-width:72px; font-size:31px; letter-spacing:-.045em; text-align:right; }
+
+/* Quick thermostat: target control gets its own row, readings no longer collide with it. */
+#dashboardZones { grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr)); }
+#dashboardZones .zone-thermostat { min-width:0; }
+#dashboardZones .thermostat-main {
+ grid-template-columns:minmax(0,1fr) minmax(0,1fr);
+ grid-template-areas:"control control" "room device";
+ gap:5px 10px;
+ margin:8px 0 7px;
+}
+#dashboardZones .thermostat-main > div:first-child { grid-area:room; min-width:0; text-align:left; }
+#dashboardZones .thermostat-main > .temperature-control { grid-area:control; }
+#dashboardZones .thermostat-main > div:last-child { grid-area:device; min-width:0; text-align:right; }
+#dashboardZones .thermostat-main small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+#dashboardZones .thermostat-main strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; }
+#dashboardZones .thermostat-main .temperature-control.compact { gap:12px; padding:0 0 2px; }
+#dashboardZones .thermostat-main .temperature-control.compact button { width:36px; height:36px; font-size:18px; }
+#dashboardZones .thermostat-main .target-temp.compact { min-width:80px; font-size:30px; }
+#dashboardZones .list-card-head h3 { font-size:16px; }
+#dashboardZones .list-card-head p { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; }
+#dashboardZones .zone-enable-toggle { flex:0 0 auto; }
+#dashboardZones .zone-state-line { gap:8px; font-size:10px; }
+#dashboardZones .zone-state-line span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+
+/* Smaller Node-RED/simulator-like plan blocks with explicit group context. */
+#controlPlan.automation-plan-grid { grid-template-columns:repeat(auto-fit,minmax(min(100%,210px),1fr)); gap:8px; }
+#controlPlan .plan-card { min-height:0; gap:7px; padding:11px; border-radius:14px; }
+#controlPlan .plan-card-head { gap:7px; }
+#controlPlan .plan-card-head h3 { margin-top:1px; font-size:15px; line-height:1.2; }
+#controlPlan .plan-card-head .eyebrow { font-size:8px; line-height:1.25; }
+#controlPlan .plan-card .badge { max-width:100px; padding:4px 6px; overflow:hidden; font-size:9px; text-overflow:ellipsis; white-space:nowrap; }
+#controlPlan .plan-card > p { font-size:10px; line-height:1.3; }
+#controlPlan .plan-temp { gap:5px; }
+#controlPlan .plan-temp > span { font-size:15px; }
+#controlPlan .plan-temp > strong { font-size:21px; }
+#controlPlan .plan-group-state { display:flex; align-items:center; gap:5px; min-width:0; color:var(--text-soft); font-size:11px; }
+#controlPlan .plan-group-state strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+#controlPlan .plan-group { border-color:color-mix(in srgb,var(--accent) 20%,var(--line)); background:color-mix(in srgb,var(--surface) 97%,var(--accent)); }
+#controlPlan .plan-events { gap:4px; padding-top:6px; }
+#controlPlan .plan-events li { grid-template-columns:62px minmax(0,1fr); gap:5px; font-size:9px; line-height:1.3; }
+#controlPlan .plan-events li span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+#controlPlan .plan-events .muted { white-space:normal; }
+
+@media (max-width: 900px) {
+ [data-view="dashboard"] .dashboard-summary-card { align-items:flex-start; }
+ [data-view="dashboard"] .dashboard-summary-status { flex-wrap:wrap; }
+ [data-view="dashboard"] .dashboard-summary-status .metrics { order:2; width:100%; justify-content:flex-end; }
+}
+
+@media (max-width: 700px) {
+ [data-view="dashboard"] .dashboard-summary-card { min-height:0; padding:12px 13px; }
+ [data-view="dashboard"] .dashboard-summary-copy h1 { font-size:18px; }
+ [data-view="dashboard"] .dashboard-summary-status { align-self:center; }
+ [data-view="dashboard"] .dashboard-summary-status .metrics { display:none; }
+ [data-view="dashboard"] .dashboard-summary-status .hero-temp { min-width:64px; font-size:27px; }
+ #dashboardZones { grid-template-columns:1fr; }
+ #controlPlan.automation-plan-grid { grid-template-columns:repeat(auto-fit,minmax(min(100%,180px),1fr)); }
+}
+[data-view="dashboard"] .dashboard-summary-card { margin-bottom:10px; }
diff --git a/web/sw.js b/web/sw.js
index f5bc303..7887d98 100644
--- a/web/sw.js
+++ b/web/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'gree-controller-v068-dashboard-wrap';
+const CACHE = 'gree-controller-v069-dashboard-fit';
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')];