v0.8.10
This commit is contained in:
+55
-7
@@ -28,7 +28,7 @@ const app = {
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
|
||||
customChartSeries: [], savedCharts: [], zoneControlSeq: {}, zoneTemperatureTimers: {},
|
||||
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, sensorAliases: {},
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false,
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main',
|
||||
};
|
||||
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
|
||||
|
||||
@@ -335,6 +335,8 @@ function renderSummary() {
|
||||
const online = app.devices.filter(d => d.online).length;
|
||||
const active = app.devices.filter(d => d.power).length;
|
||||
const demand = app.zones.filter(z => z.enabled && z.demand).length;
|
||||
const toolbarStatus = $('#toolbarRuntimeStatus');
|
||||
if (toolbarStatus) toolbarStatus.textContent = tr('dashboard.toolbarWorking', {active, total:app.devices.length});
|
||||
$('#heroTemperature').innerHTML = `${average === null ? '--' : average.toFixed(1)}<small>°C</small>`;
|
||||
const activeSuffix = active ? tr('dashboard.summaryActive', {count: active}) : '';
|
||||
$('#summaryText').textContent = app.devices.length
|
||||
@@ -804,9 +806,9 @@ function zoneControlOwnerLabel(zone) {
|
||||
|
||||
function zoneControlOwnerMeta(zone) {
|
||||
const parts=[];
|
||||
if(zone.control_since) parts.push(`${tr('zones.ownerSince')} ${new Date(zone.control_since).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'})}`);
|
||||
if(zone.control_resume_at) parts.push(`${tr('zones.ownerResume')} ${new Date(zone.control_resume_at).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'})}`);
|
||||
if(zone.lockout_until && new Date(zone.lockout_until)>new Date()) parts.push(`${tr('zones.lockoutUntil')} ${new Date(zone.lockout_until).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'})}`);
|
||||
if(zone.control_since) parts.push(`${tr('zones.ownerSince')} ${dateTime(zone.control_since)}`);
|
||||
if(zone.control_resume_at) parts.push(`${tr('zones.ownerResume')} ${dateTime(zone.control_resume_at)}`);
|
||||
if(zone.lockout_until && new Date(zone.lockout_until)>new Date()) parts.push(`${tr('zones.lockoutUntil')} ${dateTime(zone.lockout_until)}`);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
@@ -1112,6 +1114,15 @@ function renderSettings() {
|
||||
form.slack_webhook_url.placeholder=n.slack_configured?'Saved - leave empty to keep':'https://hooks.slack.com/services/…';
|
||||
form.discord_webhook_url.placeholder=n.discord_configured?'Saved - leave empty to keep':'https://discord.com/api/webhooks/…';
|
||||
form.notification_cooldown_seconds.value=n.cooldown_seconds||300; form.notification_failure_threshold.value=n.communication_failure_threshold||3; form.notification_target_timeout.value=n.target_timeout_minutes||60;
|
||||
const alerts=n.alert_types||{};
|
||||
form.notification_alert_stale_sensor.checked=alerts.stale_sensor!==false;
|
||||
form.notification_alert_sensor_errors.checked=alerts.sensor_errors!==false;
|
||||
form.notification_alert_communication.checked=alerts.communication!==false;
|
||||
form.notification_alert_target_timeout.checked=alerts.target_timeout!==false;
|
||||
form.notification_alert_automation.checked=alerts.automation!==false;
|
||||
form.notification_alert_control_errors.checked=alerts.control_errors!==false;
|
||||
form.notification_alert_important_events.checked=alerts.important_events!==false;
|
||||
form.notification_alert_other.checked=alerts.other!==false;
|
||||
updateNotificationFields();
|
||||
updateInfluxFields();
|
||||
renderLogRetention();
|
||||
@@ -1226,12 +1237,29 @@ function updateBrowserUrl(path, replace=false) {
|
||||
history[replace ? 'replaceState' : 'pushState']({}, '', target);
|
||||
}
|
||||
|
||||
function setDashboardTab(tab, {scroll=true}={}) {
|
||||
const next = ['main','thermostats','manual'].includes(tab) ? tab : 'main';
|
||||
app.dashboardTab = next;
|
||||
$$('[data-dashboard-tab]').forEach(button => {
|
||||
const active = button.dataset.dashboardTab === next;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
$$('[data-dashboard-panel]').forEach(panel => {
|
||||
const active = panel.dataset.dashboardPanel === next;
|
||||
panel.classList.toggle('active', active);
|
||||
panel.hidden = !active;
|
||||
});
|
||||
if (scroll && app.currentView === 'dashboard') window.scrollTo({top:0, behavior:'smooth'});
|
||||
}
|
||||
|
||||
function showView(name, {push=true, scroll=true}={}) {
|
||||
app.currentView = name;
|
||||
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
|
||||
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['groups','schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name))));
|
||||
if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard'));
|
||||
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
if (name === 'dashboard') setDashboardTab(app.dashboardTab, {scroll:false});
|
||||
if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
|
||||
if (name === 'logs') loadLogs();
|
||||
}
|
||||
@@ -1920,6 +1948,7 @@ function connectWebSocket() {
|
||||
|
||||
document.addEventListener('click', async event => {
|
||||
const button = event.target.closest('button'); if (!button) return;
|
||||
if (button.dataset.dashboardTab) { setDashboardTab(button.dataset.dashboardTab); return; }
|
||||
if (button.dataset.nav) {
|
||||
if (button.dataset.nav === 'more') openDialog('moreDialog'); else showView(button.dataset.nav);
|
||||
return;
|
||||
@@ -2015,9 +2044,8 @@ document.addEventListener('click', async event => {
|
||||
}
|
||||
if (action === 'zone-go-control') {
|
||||
showView('dashboard', {scroll:false});
|
||||
setDashboardTab('thermostats', {scroll:false});
|
||||
const target = [...document.querySelectorAll('[data-zone-card]')].find(card => card.dataset.zoneCard === button.dataset.id);
|
||||
const disclosure = target?.closest('details.dashboard-disclosure');
|
||||
if (disclosure) disclosure.open = true;
|
||||
requestAnimationFrame(() => target?.scrollIntoView({behavior:'smooth', block:'center'}));
|
||||
return;
|
||||
}
|
||||
@@ -2064,7 +2092,7 @@ $('#simulationOpenTab')?.addEventListener('click', () => { const params=new URLS
|
||||
$('#simulationFullscreen')?.addEventListener('click', async () => { try { if(document.fullscreenElement) await document.exitFullscreen(); else await document.documentElement.requestFullscreen(); } catch(error) { toast(error.message, true); } });
|
||||
document.addEventListener('fullscreenchange', () => { const button=$('#simulationFullscreen'); if(button) button.textContent=tr(document.fullscreenElement?'simulation.exitFullscreen':'simulation.fullscreen'); });
|
||||
|
||||
$('#testNotifications')?.addEventListener('click', async () => { const f=$('#settingsForm'); const saved=app.settings?.notifications||{}; const body={enabled:true,mode:f.notifications_mode.value,provider:f.notifications_provider.value,pushover_app_token:f.pushover_app_token.value.trim(),pushover_user_key:f.pushover_user_key.value.trim(),slack_webhook_url:f.slack_webhook_url.value.trim(),discord_webhook_url:f.discord_webhook_url.value.trim(),cooldown_seconds:Number(f.notification_cooldown_seconds.value||300),communication_failure_threshold:Number(f.notification_failure_threshold.value||3),target_timeout_minutes:Number(f.notification_target_timeout.value||60)}; try{await api('/api/integrations/notifications/test',{method:'POST',body});toast('Test notification sent');}catch(e){toast(e.message,true);} });
|
||||
$('#testNotifications')?.addEventListener('click', async () => { const f=$('#settingsForm'); const body={enabled:true,mode:f.notifications_mode.value,provider:f.notifications_provider.value,pushover_app_token:f.pushover_app_token.value.trim(),pushover_user_key:f.pushover_user_key.value.trim(),slack_webhook_url:f.slack_webhook_url.value.trim(),discord_webhook_url:f.discord_webhook_url.value.trim(),cooldown_seconds:Number(f.notification_cooldown_seconds.value||300),communication_failure_threshold:Number(f.notification_failure_threshold.value||3),target_timeout_minutes:Number(f.notification_target_timeout.value||60),alert_types:{stale_sensor:f.notification_alert_stale_sensor.checked,sensor_errors:f.notification_alert_sensor_errors.checked,communication:f.notification_alert_communication.checked,target_timeout:f.notification_alert_target_timeout.checked,automation:f.notification_alert_automation.checked,control_errors:f.notification_alert_control_errors.checked,important_events:f.notification_alert_important_events.checked,other:f.notification_alert_other.checked}}; try{await api('/api/integrations/notifications/test',{method:'POST',body});toast(tr('notifications.testSent'));}catch(e){toast(e.message,true);} });
|
||||
|
||||
$('#copyZoneSettings')?.addEventListener('click',()=>{
|
||||
const f=$('#zoneForm');
|
||||
@@ -2279,6 +2307,16 @@ function currentSettingsBody() {
|
||||
enabled: !!settings.notifications?.enabled, mode: settings.notifications?.mode || 'problems', provider: settings.notifications?.provider || 'pushover',
|
||||
pushover_app_token: '', pushover_user_key: '', slack_webhook_url: '', discord_webhook_url: '',
|
||||
cooldown_seconds: Number(settings.notifications?.cooldown_seconds || 300), communication_failure_threshold: Number(settings.notifications?.communication_failure_threshold || 3), target_timeout_minutes: Number(settings.notifications?.target_timeout_minutes || 60),
|
||||
alert_types: {
|
||||
stale_sensor: settings.notifications?.alert_types?.stale_sensor !== false,
|
||||
sensor_errors: settings.notifications?.alert_types?.sensor_errors !== false,
|
||||
communication: settings.notifications?.alert_types?.communication !== false,
|
||||
target_timeout: settings.notifications?.alert_types?.target_timeout !== false,
|
||||
automation: settings.notifications?.alert_types?.automation !== false,
|
||||
control_errors: settings.notifications?.alert_types?.control_errors !== false,
|
||||
important_events: settings.notifications?.alert_types?.important_events !== false,
|
||||
other: settings.notifications?.alert_types?.other !== false,
|
||||
},
|
||||
},
|
||||
night_mode: {
|
||||
enabled: !!settings.night_mode?.enabled,
|
||||
@@ -2343,6 +2381,16 @@ function settingsBodyFromForm(form) {
|
||||
cooldown_seconds: Number(raw.notification_cooldown_seconds || 300),
|
||||
communication_failure_threshold: Number(raw.notification_failure_threshold || 3),
|
||||
target_timeout_minutes: Number(raw.notification_target_timeout || 60),
|
||||
alert_types: {
|
||||
stale_sensor: form.notification_alert_stale_sensor.checked,
|
||||
sensor_errors: form.notification_alert_sensor_errors.checked,
|
||||
communication: form.notification_alert_communication.checked,
|
||||
target_timeout: form.notification_alert_target_timeout.checked,
|
||||
automation: form.notification_alert_automation.checked,
|
||||
control_errors: form.notification_alert_control_errors.checked,
|
||||
important_events: form.notification_alert_important_events.checked,
|
||||
other: form.notification_alert_other.checked,
|
||||
},
|
||||
};
|
||||
return body;
|
||||
}
|
||||
|
||||
+64
-32
@@ -16,6 +16,7 @@
|
||||
<div class="brand">
|
||||
<div class="brand-line"><strong>GREE Controller</strong><small id="connectionLabel" class="connection-dot connecting" role="status" aria-label="Connecting" title="Connecting"></small></div>
|
||||
</div>
|
||||
<div class="toolbar-runtime-status" id="toolbarRuntimeStatus" aria-live="polite">0/0</div>
|
||||
<div class="top-actions">
|
||||
<select class="toolbar-select" id="languageSelect" data-i18n-aria="controls.language" aria-label="Language">
|
||||
<option value="en">English</option>
|
||||
@@ -26,53 +27,70 @@
|
||||
<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">↻</button>
|
||||
<button class="primary compact" id="discoverButton" data-i18n="actions.discover">Discover</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="view active" data-view="dashboard">
|
||||
<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="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 thermostat zones. Turn all off also disables all groups; turn all on starts enabled zones only.</p></div>
|
||||
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
||||
<nav class="dashboard-tabs" data-i18n-aria="dashboard.sections" aria-label="Dashboard sections">
|
||||
<button type="button" class="active" data-dashboard-tab="main" data-i18n="dashboard.tabMain">Main</button>
|
||||
<button type="button" data-dashboard-tab="thermostats" data-i18n="dashboard.tabThermostats">Thermostats</button>
|
||||
<button type="button" data-dashboard-tab="manual" data-i18n="dashboard.tabManual">Manual control</button>
|
||||
</nav>
|
||||
|
||||
<div class="dashboard-tab-panel active" data-dashboard-panel="main">
|
||||
<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="dashboard-global-divider"></div>
|
||||
<div class="house-climate dashboard-house-climate" id="houseClimate"></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 thermostat zones. Turn all off also disables all groups; turn all on starts enabled zones only.</p></div>
|
||||
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
||||
</div>
|
||||
<div class="dashboard-global-divider"></div>
|
||||
<div class="house-climate dashboard-house-climate" id="houseClimate"></div>
|
||||
</div>
|
||||
<section class="panel dashboard-section-panel">
|
||||
<div class="dashboard-section-head compact-head">
|
||||
<div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h2 data-i18n="plan.title">What happens next</h2></div>
|
||||
</div>
|
||||
<div class="automation-plan-grid" id="controlPlan"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="dashboard-primary-section">
|
||||
<div class="dashboard-section-head">
|
||||
<div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h2 data-i18n="groups.dashboardTitle">Control selected air conditioners</h2><p data-i18n="groups.dashboardHint">Each group can use its own cooling/heating mode and Auto/Comfort/Sleep/Away profile.</p></div>
|
||||
<div class="dashboard-section-actions"><span class="badge" id="dashboardGroupCount">0</span><button type="button" class="secondary" data-go="groups" data-i18n="groups.manage">Manage groups</button></div>
|
||||
</div>
|
||||
<div class="group-quick-grid dashboard-group-strip" id="dashboardGroups"></div>
|
||||
</section>
|
||||
<div class="dashboard-tab-panel" data-dashboard-panel="thermostats" hidden>
|
||||
<section class="dashboard-primary-section">
|
||||
<div class="dashboard-section-head">
|
||||
<div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h2 data-i18n="groups.dashboardTitle">Control selected air conditioners</h2><p data-i18n="groups.dashboardHint">Each group can use its own cooling/heating mode and Auto/Comfort/Sleep/Away profile.</p></div>
|
||||
<div class="dashboard-section-actions"><span class="badge" id="dashboardGroupCount">0</span><button type="button" class="secondary" data-go="groups" data-i18n="groups.manage">Manage groups</button></div>
|
||||
</div>
|
||||
<div class="group-quick-grid dashboard-group-strip" id="dashboardGroups"></div>
|
||||
</section>
|
||||
<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>
|
||||
<span class="badge" id="dashboardZoneCount">0</span>
|
||||
</div>
|
||||
<div class="list-grid dashboard-zones" id="dashboardZones"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-secondary-stack">
|
||||
<details class="panel dashboard-disclosure">
|
||||
<summary><div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h3 data-i18n="plan.title">What happens next</h3></div><span class="dashboard-disclosure-chevron" aria-hidden="true">⌄</span></summary>
|
||||
<div class="dashboard-disclosure-body"><div class="automation-plan-grid" id="controlPlan"></div></div>
|
||||
</details>
|
||||
<details class="panel dashboard-disclosure">
|
||||
<summary><div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h3 data-i18n="dashboard.quickThermostats">Quick thermostats</h3></div><div class="dashboard-summary-side"><span class="badge" id="dashboardZoneCount">0</span><span class="dashboard-disclosure-chevron" aria-hidden="true">⌄</span></div></summary>
|
||||
<div class="dashboard-disclosure-body"><div class="list-grid dashboard-zones" id="dashboardZones"></div></div>
|
||||
</details>
|
||||
<details class="panel dashboard-disclosure">
|
||||
<summary><div><span class="eyebrow" data-i18n="nav.devices">Devices</span><h3 data-i18n="dashboard.quickControl">Direct device control</h3></div><div class="dashboard-summary-side"><span class="badge" id="dashboardDeviceCount">0</span><span class="dashboard-disclosure-chevron" aria-hidden="true">⌄</span></div></summary>
|
||||
<div class="dashboard-disclosure-body"><div class="device-grid" id="dashboardDevices"></div></div>
|
||||
</details>
|
||||
<div class="dashboard-tab-panel" data-dashboard-panel="manual" hidden>
|
||||
<section class="panel dashboard-section-panel">
|
||||
<div class="dashboard-section-head compact-head">
|
||||
<div><span class="eyebrow" data-i18n="nav.devices">Devices</span><h2 data-i18n="dashboard.quickControl">Manual control</h2></div>
|
||||
<span class="badge" id="dashboardDeviceCount">0</span>
|
||||
</div>
|
||||
<div class="device-grid" id="dashboardDevices"></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="devices">
|
||||
<div class="section-heading">
|
||||
<div><span class="eyebrow" data-i18n="devices.lan">LAN network</span><h1 data-i18n="nav.devices">Devices</h1></div>
|
||||
<button class="secondary" data-open="deviceDialog" data-i18n="devices.addManual">Add manually</button>
|
||||
<div class="section-heading-actions"><button class="primary compact" id="discoverButton" data-i18n="actions.discover">Discover</button><button class="secondary" data-open="deviceDialog" data-i18n="devices.addManual">Add manually</button></div>
|
||||
</div>
|
||||
<div class="device-grid" id="deviceList"></div>
|
||||
</section>
|
||||
@@ -237,6 +255,20 @@
|
||||
<label><span data-i18n="notifications.cooldown">Cooldown (s)</span><input type="number" name="notification_cooldown_seconds" min="30" max="86400" value="300"></label>
|
||||
<label><span data-i18n="notifications.failureThreshold">Communication failures before alert</span><input type="number" name="notification_failure_threshold" min="2" max="100" value="3"></label>
|
||||
<label><span data-i18n="notifications.targetTimeout">Target not reached timeout (min)</span><input type="number" name="notification_target_timeout" min="5" max="1440" value="60"></label>
|
||||
<fieldset class="notification-alert-types wide">
|
||||
<legend data-i18n="notifications.alertTypes">Alert types</legend>
|
||||
<p class="field-note" data-i18n="notifications.alertTypesHint">Choose exactly which alerts may be delivered. Thresholds above still define when applicable alerts are raised.</p>
|
||||
<div class="notification-alert-grid">
|
||||
<label class="check"><input type="checkbox" name="notification_alert_stale_sensor"> <span data-i18n="notifications.alertStaleSensor">Stale Home Assistant sensor readings</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_sensor_errors"> <span data-i18n="notifications.alertSensorErrors">Other Home Assistant sensor errors</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_communication"> <span data-i18n="notifications.alertCommunication">Device communication problems</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_target_timeout"> <span data-i18n="notifications.alertTargetTimeout">Target temperature timeout</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_automation"> <span data-i18n="notifications.alertAutomation">Automation errors and conflicts</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_control_errors"> <span data-i18n="notifications.alertControlErrors">Thermostat and group control errors</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_important_events"> <span data-i18n="notifications.alertImportantEvents">Important state changes</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_other"> <span data-i18n="notifications.alertOther">Other warnings and errors</span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-actions wide"><button type="button" class="secondary" id="testNotifications" data-i18n="notifications.test">Send test notification</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+32
-3
@@ -82,14 +82,15 @@ body { margin: 0; min-height: 100vh; color: var(--text); }
|
||||
button, input, select { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
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; }
|
||||
.topbar { position: sticky; top: 0; z-index: 20; display: grid; grid-template-columns: minmax(0,1fr) auto minmax(0,1fr); align-items: center; 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; justify-self:start; min-width: 0; }
|
||||
.brand strong, .brand small { display: block; }
|
||||
.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; }
|
||||
.top-actions { display: flex; align-items: center; justify-content: flex-end; justify-self:end; gap: 7px; }
|
||||
.toolbar-runtime-status { justify-self:center; padding:7px 10px; border:1px solid var(--line); border-radius:999px; color:var(--text-soft); background:var(--surface-muted); font-size:11px; font-weight:750; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
||||
.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; }
|
||||
@@ -122,6 +123,7 @@ h3 { margin-bottom: 10px; }
|
||||
.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; }
|
||||
.section-heading-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
|
||||
.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: 20px; }
|
||||
@@ -829,6 +831,33 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
||||
.temporary-active-summary .danger { width:100%; white-space:normal; }
|
||||
}
|
||||
|
||||
/* Dashboard sections: horizontal tabs, one vertically scrollable panel at a time. */
|
||||
.dashboard-tabs { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin:0 0 12px; padding:6px; border:1px solid var(--line); border-radius:17px; background:var(--surface); }
|
||||
.dashboard-tabs button { min-width:0; min-height:40px; padding:8px 10px; border:1px solid transparent; border-radius:12px; color:var(--muted); background:transparent; font-size:12px; font-weight:750; }
|
||||
.dashboard-tabs button.active { border-color:var(--accent-border); color:var(--accent); background:var(--accent-soft); }
|
||||
.dashboard-tab-panel[hidden] { display:none !important; }
|
||||
.dashboard-tab-panel.active { display:block; animation:reveal .18s ease; }
|
||||
.dashboard-section-panel { margin-bottom:18px; }
|
||||
.dashboard-section-head.compact-head { align-items:center; margin-bottom:12px; }
|
||||
.dashboard-section-head.compact-head h2 { font-size:19px; }
|
||||
.dashboard-section-panel > .device-grid,
|
||||
.dashboard-section-panel > .list-grid,
|
||||
.dashboard-section-panel > .automation-plan-grid { margin-bottom:0; }
|
||||
.notification-alert-types { display:grid; gap:10px; }
|
||||
.notification-alert-types > .field-note { margin:0; }
|
||||
.notification-alert-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:4px 14px; }
|
||||
.notification-alert-grid .check { min-height:36px; }
|
||||
|
||||
@media (max-width:700px) {
|
||||
.topbar { gap:7px; padding-inline:10px; }
|
||||
.toolbar-runtime-status { padding:6px 8px; font-size:10px; }
|
||||
.section-heading { align-items:stretch; flex-direction:column; }
|
||||
.section-heading-actions { display:grid; grid-template-columns:1fr 1fr; width:100%; }
|
||||
.dashboard-tabs { gap:5px; padding:5px; }
|
||||
.dashboard-tabs button { min-height:42px; padding:6px 5px; font-size:10px; line-height:1.15; }
|
||||
.notification-alert-grid { grid-template-columns:1fr; }
|
||||
}
|
||||
|
||||
/* 0.6.9 dashboard density and fit fixes */
|
||||
[data-view="dashboard"] .dashboard-summary-card {
|
||||
min-height:0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'gree-controller-v088-manual-takeover-confirmation';
|
||||
const CACHE = 'gree-controller-v0810-dashboard-alert-filters';
|
||||
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