v0.6.9
This commit is contained in:
+35
-11
@@ -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
@@ -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>
|
||||
|
||||
@@ -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,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')];
|
||||
|
||||
Reference in New Issue
Block a user