326 lines
23 KiB
JavaScript
326 lines
23 KiB
JavaScript
function renderAccessTokens() {
|
||
const list = $('#accessTokenList');
|
||
if (!list) return;
|
||
list.innerHTML = app.accessTokens.length ? app.accessTokens.map(item => `
|
||
<div class="token-row">
|
||
<div><strong>${esc(item.name)}</strong><small class="mono">${esc(item.token_prefix)}</small><small>${esc(tr('settings.created'))}: ${esc(dateTime(item.created_at))}</small></div>
|
||
<button type="button" class="danger" data-action="revoke-access-token" data-id="${esc(item.id)}">${esc(tr('actions.revoke'))}</button>
|
||
</div>`).join('') : `<div class="empty compact"><strong>${esc(tr('settings.noTokens'))}</strong>${esc(tr('settings.noTokensHint'))}</div>`;
|
||
}
|
||
|
||
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.flowSharedInputs || []).map(item => item?.config?.entity_id).filter(Boolean),
|
||
...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 => `<div class="sensor-alias-row"><span class="mono" title="${esc(entity)}">${esc(entity)}</span><input data-sensor-alias="${esc(entity)}" value="${esc(app.sensorAliases?.[entity] || '')}" placeholder="${esc(tr('settings.aliasPlaceholder'))}"><button type="button" class="sensor-alias-clear" data-clear-sensor-alias="${esc(entity)}" title="${esc(tr('actions.clear'))}">×</button></div>`).join('') : `<div class="empty compact">${esc(tr('settings.noSensorAliases'))}</div>`;
|
||
}
|
||
|
||
function flowSharedInputKinds() {
|
||
return [
|
||
['constant', 'flow.node.constant'], ['ha_state', 'flow.node.haState'], ['ha_numeric', 'flow.node.haNumeric'],
|
||
['ha_attribute', 'flow.node.haAttribute'], ['ha_available', 'flow.node.haAvailable'],
|
||
['outdoor_temperature', 'flow.node.outdoorTemperature'], ['device_temperature', 'flow.node.deviceTemperature'],
|
||
['zone_temperature', 'flow.node.zoneTemperature'], ['house_mode', 'flow.node.houseMode'],
|
||
['device_state', 'flow.node.deviceState'], ['zone_state', 'flow.node.zoneState'],
|
||
['group_state', 'flow.node.groupState'], ['night_mode', 'flow.node.nightMode'],
|
||
];
|
||
}
|
||
|
||
function sharedFlowInputSummary(item) {
|
||
if (!item) return '—';
|
||
try { return flowNodeSummary({ kind: item.kind, config: item.config || {} }); } catch (_) { return item.kind || '—'; }
|
||
}
|
||
|
||
function renderFlowSharedInputs() {
|
||
const host = $('#flowSharedInputList'); if (!host) return;
|
||
const items = app.flowSharedInputs || [];
|
||
host.innerHTML = items.length ? items.map(item => `<div class="flow-shared-input-row">
|
||
<div><strong>${esc(item.name)}</strong><small>${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title:item.kind }))} · ${esc(sharedFlowInputSummary(item))}</small><code>${esc(item.id)}</code></div>
|
||
<div class="flow-shared-input-actions"><button type="button" class="secondary" data-flow-shared-edit="${esc(item.id)}">${esc(tr('actions.edit'))}</button><button type="button" class="danger" data-flow-shared-delete="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div>
|
||
</div>`).join('') : `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputsEmptyHint'))}</span></div>`;
|
||
const revision = $('#flowSharedInputsRevision');
|
||
if (revision) revision.value = JSON.stringify(items);
|
||
}
|
||
|
||
function sharedFlowOptions(items, selected, label = item => item.name) {
|
||
return items.map(item => `<option value="${esc(item.id)}" ${item.id === selected ? 'selected' : ''}>${esc(label(item))}</option>`).join('');
|
||
}
|
||
|
||
function sharedFlowOperatorOptions(selected = 'eq') {
|
||
return [['lt','<'],['lte','≤'],['gt','>'],['gte','≥'],['eq','='],['neq','≠']].map(([value,label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('');
|
||
}
|
||
|
||
function renderFlowSharedInputFields(kind, config = {}) {
|
||
const host = $('#flowSharedInputFields'); if (!host) return;
|
||
const c = config || {};
|
||
const comparison = (temperature = false) => `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator">${sharedFlowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-shared-config="value" value="${Number(c.value ?? 0)}"></label></div>`;
|
||
const textComparison = () => `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.value'))}</span><input data-shared-config="value" value="${esc(c.value ?? '')}"></label></div>`;
|
||
let fields = '';
|
||
if (kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-shared-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
|
||
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>${textComparison()}`;
|
||
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>${comparison(false)}`;
|
||
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator">${sharedFlowOperatorOptions(c.operator || 'eq')}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-shared-config="value" value="${esc(c.value ?? '')}"></label></div>`;
|
||
else if (kind === 'ha_available') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
|
||
else if (kind === 'outdoor_temperature') fields = comparison(true);
|
||
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>${comparison(true)}`;
|
||
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>${comparison(true)}`;
|
||
else if (kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.houseMode'))}</span><select data-shared-config="value">${['cool','heat','off'].map(v => `<option value="${v}" ${c.value === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select></label></div>`;
|
||
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','online','power','mode','fan_speed','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${textComparison()}`;
|
||
else if (kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','mode','active_preset','demand','control_owner','device_manual_override','local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${textComparison()}`;
|
||
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">${textComparison()}`;
|
||
else if (kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||
host.innerHTML = fields;
|
||
}
|
||
|
||
function openFlowSharedInputEditor(id = '') {
|
||
const form = $('#flowSharedInputForm'), dialog = $('#flowSharedInputDialog'); if (!form || !dialog) return;
|
||
const item = id ? (app.flowSharedInputs || []).find(value => value.id === id) : null;
|
||
const kindSelect = form.kind;
|
||
kindSelect.innerHTML = flowSharedInputKinds().map(([kind,key]) => `<option value="${kind}">${esc(tr(key))}</option>`).join('');
|
||
form.id.value = item?.id || '';
|
||
form.name.value = item?.name || '';
|
||
form.kind.value = item?.kind || 'constant';
|
||
form.dataset.editingId = item?.id || '';
|
||
renderFlowSharedInputFields(form.kind.value, item?.config || flowDefaultConfig(form.kind.value));
|
||
dialog.showModal();
|
||
setTimeout(() => form.name.focus(), 0);
|
||
}
|
||
|
||
function collectFlowSharedInputConfig(kind) {
|
||
const config = {};
|
||
$$('[data-shared-config]', $('#flowSharedInputFields')).forEach(field => {
|
||
const key = field.dataset.sharedConfig;
|
||
let value = field.value;
|
||
if (kind === 'constant' && key === 'value') value = value === 'true';
|
||
else if (field.type === 'number') value = Number(value);
|
||
config[key] = value;
|
||
});
|
||
return config;
|
||
}
|
||
|
||
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 = `<div class="system-panel-head"><div><span class="eyebrow">${esc(tr('settings.systemState'))}</span><h3>${esc(tr('settings.systemStatusTitle'))}</h3><p>${esc(tr('settings.systemStateHint'))}</p></div><span class="system-health ${ready && connected ? 'ok' : 'warn'}">${esc(ready && connected ? tr('settings.healthy') : tr('settings.attention'))}</span></div><div class="system-status-grid">${items.map(([label, value, tone]) => `<div class="system-status-item ${esc(tone)}"><small>${esc(label)}</small><strong>${esc(value)}</strong></div>`).join('')}</div>`;
|
||
}
|
||
|
||
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 `<div class="gree-frame-stat"><small>${esc(device.name || device.id)}</small><strong>${count.toLocaleString(locale())}</strong></div>`;
|
||
}).join('');
|
||
host.innerHTML = `<div class="gree-frame-stat total"><small>${esc(tr('settings.receivedFramesTotal'))}</small><strong>${total.toLocaleString(locale())}</strong></div>${deviceRows || `<div class="gree-frame-stat"><small>${esc(tr('settings.receivedFramesDevices'))}</small><strong>0</strong></div>`}`;
|
||
}
|
||
|
||
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();
|
||
renderFlowSharedInputs();
|
||
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 `<div class="debug-line source-${sourceClass}"><time>${esc(new Date(line.timestamp).toLocaleTimeString(locale()))}</time><b>${esc(line.source)}</b><span>${esc(line.kind)}: ${esc(line.message || '')}${esc(details)}</span></div>`;
|
||
}).join('') : `<div class="debug-empty">${esc(tr(app.debugFilter === 'gree' ? 'debug.emptyGree' : app.debugFilter === 'requests' ? 'debug.emptyRequests' : 'debug.empty'))}</div>`;
|
||
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);
|
||
|