function renderAccessTokens() { const list = $('#accessTokenList'); if (!list) return; list.innerHTML = app.accessTokens.length ? app.accessTokens.map(item => `
${esc(item.name)}${esc(item.token_prefix)}${esc(tr('settings.created'))}: ${esc(dateTime(item.created_at))}
`).join('') : `
${esc(tr('settings.noTokens'))}${esc(tr('settings.noTokensHint'))}
`; } function knownHaEntities() { return [...new Set([ ...Object.keys(app.sensorAliases || {}), ...app.zones.map(zone => zone.ha_entity_id).filter(Boolean), app.settings?.home_assistant?.default_entity_id, app.settings?.home_assistant?.outdoor_entity_id, ...app.historyData.sensors.map(row => row.entity_id), ].filter(Boolean))].sort(); } function renderSensorAliases() { const host = $('#sensorAliasList'); if (!host) return; const entities = knownHaEntities(); host.innerHTML = entities.length ? entities.map(entity => `
${esc(entity)}
`).join('') : `
${esc(tr('settings.noSensorAliases'))}
`; } function renderLogRetention() { const select = $('#logRetentionDays'); if (!select || !app.settings) return; [...select.options].forEach(option => { option.textContent = `${option.value} ${tr('common.days')}`; }); const days = String(app.settings.event_log_retention_days || 30); if (![...select.options].some(option => option.value === days)) { const option = document.createElement('option'); option.value = days; option.textContent = `${days} ${tr('common.days')}`; select.appendChild(option); } select.value = days; } function renderSettings() { if (!app.settings) return; const form = $('#settingsForm'); if (!form) return; form.controller_id.value = app.settings.controller_id || ''; form.poll_interval_seconds.value = app.settings.poll_interval_seconds || 15; form.zone_interval_seconds.value = app.settings.zone_interval_seconds || 5; form.discovery_broadcast.value = app.settings.discovery_broadcast || '255.255.255.255:7000'; form.discovery_timeout_ms.value = app.settings.discovery_timeout_ms || 3000; form.simulator_enabled.checked = !!app.settings.simulator_enabled; form.history_retention_days.value = app.settings.history_retention_days || 30; form.event_log_retention_days.value = app.settings.event_log_retention_days || 30; form.history_compaction_enabled.checked = app.settings.history_compaction_enabled !== false; form.suppress_device_beep.checked = !!app.settings.suppress_device_beep; form.compressor_protection_enabled.checked = app.settings.compressor_protection_enabled !== false; form.compressor_protection_minutes.value = (Number(app.settings.compressor_protection_seconds || 180) / 60).toFixed(1).replace(/\.0$/, ''); updateCompressorProtectionFields(); form.influx_enabled.checked = !!app.settings.influxdb?.enabled; form.influx_version.value = String(app.settings.influxdb?.version || '2'); form.influx_threshold_days.value = app.settings.influxdb?.history_threshold_days || 30; form.influx_url.value = app.settings.influxdb?.url || ''; form.influx_database.value = app.settings.influxdb?.database || 'gree_controller'; form.influx_username.value = app.settings.influxdb?.username || ''; form.influx_password.value = ''; form.influx_password.placeholder = app.settings.influxdb?.password_configured ? tr('settings.secretSaved') : tr('settings.secretKeep'); form.influx_org.value = app.settings.influxdb?.org || ''; form.influx_bucket.value = app.settings.influxdb?.bucket || 'gree_controller'; form.influx_token.value = ''; form.influx_token.placeholder = app.settings.influxdb?.token_configured ? tr('settings.secretSaved') : tr('settings.secretKeep'); form.debug_overlay_enabled.checked = !!app.settings.debug?.overlay_enabled; form.debug_gree_frames.checked = !!app.settings.debug?.gree_frames; const n=app.settings.notifications||{}; form.notifications_enabled.checked=!!n.enabled; form.notifications_mode.value=n.mode||'problems'; form.notifications_provider.value=n.provider||'pushover'; form.pushover_app_token.value=''; form.pushover_user_key.value=''; form.slack_webhook_url.value=''; form.discord_webhook_url.value=''; form.pushover_app_token.placeholder=n.pushover_configured?tr('settings.secretSaved'):tr('notifications.applicationTokenPlaceholder'); form.pushover_user_key.placeholder=n.pushover_configured?tr('settings.secretSaved'):tr('notifications.userKeyPlaceholder'); form.slack_webhook_url.placeholder=n.slack_configured?tr('settings.secretSaved'):'https://hooks.slack.com/services/…'; form.discord_webhook_url.placeholder=n.discord_configured?tr('settings.secretSaved'):'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(); renderGreeFrameStats(); renderSystemInfo(); renderSimulationModeBanner(); setSettingsTab(app.settingsTab); markFormClean(form); } function updateCompressorProtectionFields() { const form = $('#settingsForm'); if (!form?.compressor_protection_enabled || !form?.compressor_protection_minutes) return; form.compressor_protection_minutes.disabled = !form.compressor_protection_enabled.checked; } function renderSimulationModeBanner() { const banner = $('#simulationModeBanner'); if (!banner) return; const enabled = !!app.settings?.simulator_enabled; banner.hidden = !enabled; document.body.classList.toggle('simulation-mode-enabled', enabled); } function setSettingsTab(tab) { app.settingsTab = tab === 'gree' ? 'gree' : 'app'; $$('[data-settings-pane]').forEach(pane => { pane.hidden = pane.dataset.settingsPane !== app.settingsTab; }); $$('[data-settings-tab]').forEach(button => { const active = button.dataset.settingsTab === app.settingsTab; button.classList.toggle('active', active); button.setAttribute('aria-selected', String(active)); }); } function renderSystemInfo() { const host = $('#systemInfo'); if (!host) return; const elapsed = Math.max(0, Math.floor((Date.now() - Number(app.systemSnapshotAt || Date.now())) / 1000)); const uptime = Number(app.system?.uptime_seconds || 0) + elapsed; const dbPath = String(app.system?.database || ''); const database = dbPath ? dbPath.split(/[\\/]/).filter(Boolean).pop() : '—'; const ready = !!app.system?.control_ready; const connected = app.connectionStatus === 'connected'; const items = [ [tr('settings.version'), app.system?.version || '—', 'version'], [tr('settings.uptime'), formatDuration(uptime), 'uptime'], [tr('settings.controlEngine'), ready ? tr('settings.ready') : tr('settings.syncing'), ready ? 'ok' : 'warn'], [tr('settings.websocket'), tr(`status.${app.connectionStatus}`), connected ? 'ok' : 'warn'], [tr('settings.apiAuth'), app.system?.auth_required ? tr('settings.enabled') : tr('settings.disabled'), 'neutral'], [tr('settings.httpBind'), app.system?.bind || '—', 'mono'], [tr('settings.database'), database, 'mono'], [tr('settings.basePath'), app.system?.base_path || APP_BASE || '/', 'mono'], ]; host.innerHTML = `
${esc(tr('settings.systemState'))}

${esc(tr('settings.systemStatusTitle'))}

${esc(tr('settings.systemStateHint'))}

${esc(ready && connected ? tr('settings.healthy') : tr('settings.attention'))}
${items.map(([label,value,tone]) => `
${esc(label)}${esc(value)}
`).join('')}
`; } function renderGreeFrameStats() { const host = $('#greeFrameStats'); if (!host) return; const total = Number(app.system?.gree_received_frames || 0); const byDevice = app.system?.gree_received_frames_by_device || {}; const deviceRows = app.devices.filter(device => !device.simulated).map(device => { const count = Number(byDevice[device.id] || 0); return `
${esc(device.name || device.id)}${count.toLocaleString(locale())}
`; }).join(''); host.innerHTML = `
${esc(tr('settings.receivedFramesTotal'))}${total.toLocaleString(locale())}
${deviceRows || `
${esc(tr('settings.receivedFramesDevices'))}0
`}`; } function renderNightSettings() { if (!app.settings) return; const form = $('#nightModeForm'); if (!form) return; form.night_mode_enabled.checked = !!app.settings.night_mode?.enabled; form.night_mode_start.value = app.settings.night_mode?.start_time || '22:00'; form.night_mode_end.value = app.settings.night_mode?.end_time || '06:00'; form.night_mode_max_fan_speed.value = String(app.settings.night_mode?.max_fan_speed || 1); form.night_mode_force_quiet.checked = app.settings.night_mode?.force_quiet !== false; form.night_mode_native_sleep.checked = app.settings.night_mode?.use_native_sleep !== false; markFormClean(form); } function renderHomeAssistantSettings() { if (!app.settings) return; const form = $('#homeAssistantForm'); if (!form) return; form.ha_url.value = app.settings.home_assistant?.url || ''; form.ha_token.value = ''; form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken'); form.ha_entity_id.value = app.settings.home_assistant?.default_entity_id || ''; form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || ''; form.ha_sensor_stale_after_minutes.value = String(Math.max(1, Math.round(Number(app.settings.home_assistant?.sensor_stale_after_seconds || 300) / 60))); form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls; form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled; renderSensorAliases(); renderAccessTokens(); markFormClean(form); } function updateInfluxFields() { const version = $('#settingsForm [name=influx_version]')?.value || '2'; $$('[data-influx-fields]').forEach(node => { node.hidden = node.dataset.influxFields !== version; }); } function updateNotificationFields(){ const provider=$('#settingsForm [name=notifications_provider]')?.value||'pushover'; $$('[data-notification-provider]').forEach(n=>n.hidden=n.dataset.notificationProvider!==provider); } function logCategory(kind=''){ const prefix=String(kind).split('.')[0]; return ['device','zone','automation','settings'].includes(prefix)?prefix:(['home_assistant','influx','notification'].includes(prefix)?'integration':'system'); } function debugLine(source, kind, message, timestamp = new Date().toISOString(), data = null) { app.debugLines.push({source, kind, message, timestamp, data}); if (app.debugLines.length > 160) app.debugLines.splice(0, app.debugLines.length - 160); renderDebugOverlay(); } function renderDebugOverlay() { const overlay = $('#debugOverlay'); if (!overlay) return; const enabled = !!app.settings?.debug?.overlay_enabled; overlay.hidden = !enabled; if (!enabled) return; if (!['all','requests','gree'].includes(app.debugFilter)) app.debugFilter = 'all'; const status = $('#debugOverlayStatus'); if (status) status.textContent = app.settings?.debug?.gree_frames ? tr('debug.apiAndGree') : tr('debug.apiOnly'); $$('[data-debug-filter]', overlay).forEach(button => { const active = button.dataset.debugFilter === app.debugFilter; button.classList.toggle('active', active); button.setAttribute('aria-selected', String(active)); }); const host = $('#debugOverlayLines'); if (!host) return; const visible = app.debugLines.filter(line => { if (app.debugFilter === 'gree') return line.source === 'GREE'; if (app.debugFilter === 'requests') return line.source === 'HTTP'; return true; }).slice(-120); host.innerHTML = visible.length ? visible.map(line => { const details = line.data == null ? '' : ` ${typeof line.data === 'string' ? line.data : JSON.stringify(line.data)}`; const sourceClass = line.source === 'GREE' ? 'gree' : line.source === 'HTTP' ? 'request' : 'api'; return `
${esc(line.source)}${esc(line.kind)}: ${esc(line.message || '')}${esc(details)}
`; }).join('') : `
${esc(tr(app.debugFilter === 'gree' ? 'debug.emptyGree' : app.debugFilter === 'requests' ? 'debug.emptyRequests' : 'debug.empty'))}
`; host.scrollTop = host.scrollHeight; } async function loadDebugBacklog() { if (app.debugBacklogLoaded || !app.settings?.debug?.overlay_enabled) return; try { const data = await api('/api/events?limit=60'); app.debugLines = (data.events || []).reverse().map(item => ({source:'API', kind:item.kind, message:item.message, timestamp:item.timestamp, data:item.metadata})).slice(-120); app.debugBacklogLoaded = true; renderDebugOverlay(); } catch (_) {} } function formatDuration(seconds) { const days = Math.floor(seconds / 86400), hours = Math.floor((seconds % 86400) / 3600), minutes = Math.floor((seconds % 3600) / 60); return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`; } setInterval(() => { if (app.currentView === 'settings') renderSystemInfo(); }, 30000);