v0.9.1
This commit is contained in:
+12
-2
@@ -87,7 +87,7 @@
|
||||
<section class="panel dashboard-section-panel">
|
||||
<div class="dashboard-section-head compact-head">
|
||||
<div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Thermostats</h2></div>
|
||||
<div class="dashboard-section-actions"><button type="button" class="secondary" id="cancelAllCompressorTasks" data-compressor-cancel-all data-action="cancel-all-compressor-tasks" data-i18n="zones.cancelAllQueued" hidden>Cancel all waiting</button><span class="badge" id="dashboardZoneCount">0</span></div>
|
||||
<div class="dashboard-section-actions"><button type="button" class="secondary queue-overview-button" data-action="open-compressor-queue"><span aria-hidden="true">⏳</span><span data-i18n="zones.queueOverview">Queue</span><b class="queue-overview-count" data-compressor-queue-count>0</b></button><span class="badge" id="dashboardZoneCount">0</span></div>
|
||||
</div>
|
||||
<div class="list-grid dashboard-zones" id="dashboardZones"></div>
|
||||
</section>
|
||||
@@ -113,7 +113,7 @@
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="zones">
|
||||
<div class="section-heading"><div><span class="eyebrow" data-i18n="zones.automation">Automation</span><h1 data-i18n="nav.zones">Zones</h1></div><div class="section-heading-actions"><button type="button" class="secondary" data-compressor-cancel-all data-action="cancel-all-compressor-tasks" data-i18n="zones.cancelAllQueued" hidden>Cancel all waiting</button><button class="secondary" data-open="zoneDialog" data-i18n="zones.new">New zone</button></div></div>
|
||||
<div class="section-heading"><div><span class="eyebrow" data-i18n="zones.automation">Automation</span><h1 data-i18n="nav.zones">Zones</h1></div><div class="section-heading-actions"><button type="button" class="secondary queue-overview-button" data-action="open-compressor-queue"><span aria-hidden="true">⏳</span><span data-i18n="zones.queueOverview">Queue</span><b class="queue-overview-count" data-compressor-queue-count>0</b></button><button class="secondary" data-open="zoneDialog" data-i18n="zones.new">New zone</button></div></div>
|
||||
<p class="lead" data-i18n="zones.description">Zones define thermostat logic and sensor configuration. Use Quick thermostats on the Dashboard for current temperature changes and presets.</p>
|
||||
<div class="list-grid" id="zoneList"></div>
|
||||
</section>
|
||||
@@ -457,6 +457,16 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="compressorQueueDialog" class="compressor-queue-dialog">
|
||||
<div class="dialog-form compressor-queue-dialog-body">
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="zones.queueEyebrow">Compressor protection</span><h2 data-i18n="zones.queueTitle">Command queue</h2></div><button type="button" data-close>×</button></div>
|
||||
<p class="field-note" data-i18n="zones.queueHint">Commands waiting for the compressor protection window to end. You can cancel one task or clear the whole queue.</p>
|
||||
<div class="compressor-queue-summary"><span data-i18n="zones.queuePendingLabel">Waiting</span><strong id="compressorQueueModalCount">0</strong></div>
|
||||
<div id="compressorQueueList" class="compressor-queue-list"></div>
|
||||
<div class="form-actions compressor-queue-actions"><button type="button" class="danger" id="compressorQueueCancelAll" data-action="cancel-all-compressor-tasks" data-i18n="zones.cancelAllQueued">Cancel all waiting</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="temporaryThermostatDialog">
|
||||
<form method="dialog" id="temporaryThermostatForm" class="dialog-form temporary-thermostat-form">
|
||||
<input type="hidden" name="zone_id">
|
||||
|
||||
+65
-4
@@ -126,6 +126,68 @@ function compressorQueuePanel(zone) {
|
||||
return `<div class="compressor-queue-panel"><div><strong>${esc(tr('zones.compressorQueueTitle'))}</strong><span>${esc(compressorPendingDescription(zone))}</span><small>${esc(tr('zones.compressorQueueUntil', {time:when}))}</small></div><button type="button" class="secondary" data-action="cancel-compressor-task" data-id="${esc(zone.id)}">${esc(tr('actions.cancel'))}</button></div>`;
|
||||
}
|
||||
|
||||
function compressorQueueRemainingText(until) {
|
||||
const timestamp = until ? new Date(until).getTime() : NaN;
|
||||
if (!Number.isFinite(timestamp)) return tr('zones.afterProtection');
|
||||
const seconds = Math.max(0, Math.ceil((timestamp - Date.now()) / 1000));
|
||||
return seconds > 0 ? tr('zones.queueRemaining', {time:formatExtendedCountdown(seconds)}) : tr('zones.queueReady');
|
||||
}
|
||||
|
||||
function renderCompressorQueueModal() {
|
||||
const list = $('#compressorQueueList');
|
||||
const countNode = $('#compressorQueueModalCount');
|
||||
const cancelAll = $('#compressorQueueCancelAll');
|
||||
if (!list || !countNode || !cancelAll) return;
|
||||
const pending = app.zones
|
||||
.filter(zone => !!zone.compressor_pending_action)
|
||||
.slice()
|
||||
.sort((a,b) => {
|
||||
const at = new Date(a.compressor_pending_until || 0).getTime();
|
||||
const bt = new Date(b.compressor_pending_until || 0).getTime();
|
||||
return (Number.isFinite(at) ? at : Number.MAX_SAFE_INTEGER) - (Number.isFinite(bt) ? bt : Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
countNode.textContent = String(pending.length);
|
||||
cancelAll.disabled = pending.length === 0;
|
||||
cancelAll.hidden = pending.length === 0;
|
||||
cancelAll.textContent = pending.length ? `${tr('zones.cancelAllQueued')} (${pending.length})` : tr('zones.cancelAllQueued');
|
||||
if (!pending.length) {
|
||||
list.innerHTML = `<div class="compressor-queue-empty"><span aria-hidden="true">✓</span><strong>${esc(tr('zones.queueEmptyTitle'))}</strong><p>${esc(tr('zones.queueEmptyText'))}</p></div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = pending.map((zone,index) => {
|
||||
const device = app.devices.find(item => item.id === zone.device_id);
|
||||
const until = zone.compressor_pending_until || zone.lockout_until;
|
||||
const group = groupControlForZone(zone);
|
||||
const source = group ? `${zoneControlOwnerLabel(zone)}: ${group.name}` : zoneControlOwnerLabel(zone);
|
||||
const exact = until ? dateTime(until) : tr('zones.afterProtection');
|
||||
return `<article class="compressor-queue-item">
|
||||
<div class="compressor-queue-index">${index + 1}</div>
|
||||
<div class="compressor-queue-item-main">
|
||||
<div class="compressor-queue-item-head"><div><span class="eyebrow">${esc(zone.name)}</span><h3>${esc(device?.name || tr('common.noDevice'))}</h3></div><span class="compressor-queue-reason">${esc(tr('zones.queueReasonProtection'))}</span></div>
|
||||
<p class="compressor-queue-command">${esc(compressorPendingDescription(zone))}</p>
|
||||
<div class="compressor-queue-meta">
|
||||
<span>${esc(tr('zones.queueSource', {source}))}</span>
|
||||
<span>${esc(tr('zones.compressorQueueUntil', {time:exact}))}</span>
|
||||
<strong data-compressor-queue-countdown="${esc(until || '')}">${esc(compressorQueueRemainingText(until))}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="secondary compressor-queue-cancel" data-action="cancel-compressor-task" data-id="${esc(zone.id)}">${esc(tr('actions.cancel'))}</button>
|
||||
</article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateCompressorQueueCountdowns() {
|
||||
$$('[data-compressor-queue-countdown]').forEach(node => {
|
||||
node.textContent = compressorQueueRemainingText(node.dataset.compressorQueueCountdown);
|
||||
});
|
||||
}
|
||||
|
||||
function openCompressorQueueDialog() {
|
||||
renderCompressorQueueModal();
|
||||
openDialog('compressorQueueDialog');
|
||||
updateCompressorQueueCountdowns();
|
||||
}
|
||||
|
||||
function zoneControlOwnerLabel(zone) {
|
||||
const owner=zone.control_owner || (zone.device_manual_override ? 'direct_manual' : (zone.local_thermostat_power != null ? 'local_thermostat' : 'automation'));
|
||||
const source=zone.control_source || '';
|
||||
@@ -265,10 +327,9 @@ function renderZones() {
|
||||
const dashboardCount = $('#dashboardZoneCount');
|
||||
if (dashboardCount) dashboardCount.textContent = String(app.zones.length);
|
||||
const pendingCount = app.zones.filter(zone => !!zone.compressor_pending_action).length;
|
||||
$$('[data-compressor-cancel-all]').forEach(cancelAll => {
|
||||
cancelAll.hidden = pendingCount === 0;
|
||||
cancelAll.textContent = pendingCount ? `${tr('zones.cancelAllQueued')} (${pendingCount})` : tr('zones.cancelAllQueued');
|
||||
});
|
||||
$$('[data-compressor-queue-count]').forEach(node => { node.textContent = String(pendingCount); });
|
||||
$$('.queue-overview-button').forEach(button => button.classList.toggle('has-items', pendingCount > 0));
|
||||
renderCompressorQueueModal();
|
||||
}
|
||||
|
||||
function groupZones(group) {
|
||||
|
||||
+2
-1
@@ -144,8 +144,9 @@ document.addEventListener('click', async event => {
|
||||
if(!Number.isFinite(value) || value<8 || value>30) return toast(tr('groups.customTemperatureRange'),true);
|
||||
return sendGroupControl(button.dataset.id,{preset:'custom',setpoint:Math.round(value*10)/10});
|
||||
}
|
||||
if (action === 'open-compressor-queue') return openCompressorQueueDialog();
|
||||
if (action === 'cancel-compressor-task') return cancelCompressorTask(button.dataset.id);
|
||||
if (action === 'cancel-all-compressor-tasks') return cancelAllCompressorTasks();
|
||||
if (action === 'cancel-all-compressor-tasks') { if (!confirm(tr('zones.queueCancelAllConfirm', {count:app.zones.filter(zone => !!zone.compressor_pending_action).length}))) return; return cancelAllCompressorTasks(); }
|
||||
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
|
||||
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
|
||||
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; }
|
||||
|
||||
@@ -17,4 +17,5 @@ async function startApplication() {
|
||||
|
||||
setInterval(updateLocalThermostatCountdowns, 1000);
|
||||
setInterval(updateTemporaryThermostatCountdowns, 1000);
|
||||
setInterval(updateCompressorQueueCountdowns, 1000);
|
||||
startApplication().catch(error => console.error('Application startup failed:', error));
|
||||
|
||||
@@ -1421,3 +1421,32 @@ textarea[aria-invalid="true"] {
|
||||
.compressor-queue-panel strong { font-size:.88rem; }
|
||||
.compressor-queue-panel span,.compressor-queue-panel small { overflow-wrap:anywhere; }
|
||||
.compressor-queue-panel button { flex:0 0 auto; }
|
||||
|
||||
|
||||
/* v0.9.1: global compressor queue overview. */
|
||||
.queue-overview-button { display:inline-flex; align-items:center; gap:7px; }
|
||||
.queue-overview-count { display:inline-grid; place-items:center; min-width:23px; height:23px; padding:0 6px; border-radius:999px; background:color-mix(in srgb,var(--text) 9%,transparent); font-size:.78rem; }
|
||||
.queue-overview-button.has-items { border-color:var(--warning, #b7791f); box-shadow:0 0 0 1px color-mix(in srgb,var(--warning, #b7791f) 22%,transparent); }
|
||||
.queue-overview-button.has-items .queue-overview-count { background:color-mix(in srgb,var(--warning, #b7791f) 18%,transparent); color:var(--text); }
|
||||
.compressor-queue-dialog { width:min(780px, calc(100% - 24px)); }
|
||||
.compressor-queue-dialog-body { gap:16px; }
|
||||
.compressor-queue-summary { display:flex; align-items:center; justify-content:space-between; padding:10px 13px; border:1px solid var(--line); border-radius:14px; background:var(--surface); }
|
||||
.compressor-queue-summary span { color:var(--muted); font-size:.86rem; }
|
||||
.compressor-queue-summary strong { font-size:1.15rem; }
|
||||
.compressor-queue-list { display:grid; gap:10px; }
|
||||
.compressor-queue-item { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; padding:14px; border:1px solid var(--line); border-radius:16px; background:linear-gradient(135deg,color-mix(in srgb,var(--warning, #b7791f) 7%,var(--surface)),var(--surface)); }
|
||||
.compressor-queue-index { display:grid; place-items:center; width:32px; height:32px; border-radius:10px; background:color-mix(in srgb,var(--warning, #b7791f) 15%,transparent); font-weight:800; }
|
||||
.compressor-queue-item-main { min-width:0; display:grid; gap:8px; }
|
||||
.compressor-queue-item-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }
|
||||
.compressor-queue-item-head h3 { margin:2px 0 0; font-size:1rem; }
|
||||
.compressor-queue-reason { flex:0 0 auto; padding:5px 8px; border-radius:999px; background:color-mix(in srgb,var(--warning, #b7791f) 14%,transparent); font-size:.73rem; font-weight:700; }
|
||||
.compressor-queue-command { margin:0; font-weight:700; overflow-wrap:anywhere; }
|
||||
.compressor-queue-meta { display:flex; flex-wrap:wrap; gap:6px 12px; color:var(--muted); font-size:.78rem; }
|
||||
.compressor-queue-meta strong { color:var(--text); font-weight:700; }
|
||||
.compressor-queue-cancel { align-self:center; }
|
||||
.compressor-queue-empty { display:grid; justify-items:center; gap:7px; padding:30px 18px; text-align:center; border:1px dashed var(--line); border-radius:16px; color:var(--muted); }
|
||||
.compressor-queue-empty > span { display:grid; place-items:center; width:42px; height:42px; border-radius:999px; background:var(--surface); font-size:1.35rem; color:var(--text); }
|
||||
.compressor-queue-empty strong { color:var(--text); font-size:1rem; }
|
||||
.compressor-queue-empty p { margin:0; max-width:440px; }
|
||||
.compressor-queue-actions { position:sticky; bottom:0; padding-top:10px; background:linear-gradient(transparent,var(--dialog-bg) 28%); }
|
||||
@media (max-width:620px) { .compressor-queue-item { grid-template-columns:auto minmax(0,1fr); } .compressor-queue-cancel { grid-column:1 / -1; width:100%; } .compressor-queue-item-head { display:grid; } .compressor-queue-reason { justify-self:start; } }
|
||||
|
||||
Reference in New Issue
Block a user