This commit is contained in:
Mateusz Gruszczyński
2026-08-25 18:25:55 +02:00
parent 26d8f9f83a
commit a8b80bdba9
12 changed files with 150 additions and 33 deletions
+35 -11
View File
@@ -285,18 +285,42 @@ function renderControlPlan() {
host.innerHTML = `<div class="panel plan-loading">${esc(tr('plan.loading'))}</div>`;
return;
}
const houseEvents = (plan.next_events || []).slice(0, 5);
const house = `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc(plan.control_strategy || 'setpoint')}</span></div><p>${esc(tr('plan.houseSummary', {zones:(plan.zones || []).filter(zone=>zone.enabled).length, demand:(plan.zones || []).filter(zone=>zone.enabled && zone.demand).length}))}</p><ul class="plan-events">${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
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 `<article class="panel plan-card ${zone.enabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(zone.device_name || tr('common.noDevice'))}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.enabled && zone.demand ? 'active' : ''}">${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
}).join('');
const rules = (plan.rules || []).filter(rule => rule.enabled);
const ruleCard = rules.length ? `<article class="panel plan-card"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', {count:rules.length}))}</h3></div></div><ul class="plan-events">${rules.slice(0,5).map(rule=>`<li><time>${esc(automationTriggerLabel(rule))}</time><span>${esc(rule.name)}${esc(rule.action_device_name || '')}</span></li>`).join('')}</ul></article>` : '';
host.innerHTML = house + zones + ruleCard;
}
const allZones = plan.zones || [];
const houseEvents = (plan.next_events || []).slice(0, 3);
const house = `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc(plan.control_strategy || 'setpoint')}</span></div><p>${esc(tr('plan.houseSummary', {zones:allZones.filter(zone=>zone.enabled).length, demand:allZones.filter(zone=>zone.enabled && zone.demand).length}))}</p><ul class="plan-events">${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
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 `<article class="panel plan-card plan-group ${powerEnabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3></div><span class="badge ${powerEnabled ? 'active' : ''}">${esc(powerEnabled ? tr('common.on') : tr('common.off'))}</span></div><div class="plan-group-state"><strong>${esc(mode)}</strong><span>·</span><strong>${esc(preset)}</strong></div><p>${esc(tr('plan.groupSummary', {zones:members.length, demand}))}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr(powerEnabled ? 'plan.noEvents' : 'plan.groupOff'))}</li>`}</ul></article>`;
}).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 `<article class="panel plan-card plan-zone ${zone.enabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.enabled && zone.demand ? 'active' : ''}">${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
}).join('');
const rules = (plan.rules || []).filter(rule => rule.enabled);
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', {count:rules.length}))}</h3></div></div><ul class="plan-events">${rules.slice(0,3).map(rule=>`<li><time>${esc(automationTriggerLabel(rule))}</time><span>${esc(rule.name)}${esc(rule.action_group_name ? `${tr('groups.group')}: ${rule.action_group_name}` : (rule.action_device_name || ''))}</span></li>`).join('')}</ul></article>` : '';
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); }
+3 -4
View File
@@ -32,11 +32,10 @@
<main>
<section class="view active" data-view="dashboard">
<div class="hero">
<div><span class="eyebrow" data-i18n="dashboard.home">Home</span><h1 data-i18n="dashboard.title">Comfort under control</h1><p id="summaryText" data-i18n="dashboard.loading">Loading devices…</p></div>
<div class="hero-temp" id="heroTemperature">--<small>°C</small></div>
<div class="hero dashboard-summary-card">
<div class="dashboard-summary-copy"><span class="eyebrow" data-i18n="dashboard.home">Home</span><h1 data-i18n="dashboard.title">Comfort under control</h1><p id="summaryText" data-i18n="dashboard.loading">Loading devices…</p></div>
<div class="dashboard-summary-status"><div class="metrics" id="metrics"></div><div class="hero-temp" id="heroTemperature">--<small>°C</small></div></div>
</div>
<div class="metrics" id="metrics"></div>
<div class="panel dashboard-global-panel">
<div class="dashboard-global-power">
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Master power for all enabled units. Whole-house mode or profile selection automatically switches it back on.</p></div>
+79
View File
@@ -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; }
+1 -1
View File
@@ -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')];