iobroker.eos-admin 7.9.51 → 7.9.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,15 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- window.NEXOWATT_EOS_OBJECTS_STATE_TOOLS_VERSION = 'v51-unrestricted-dp-write-fix';
4
+ window.NEXOWATT_EOS_OBJECTS_STATE_TOOLS_VERSION = 'v52-fast-unrestricted-dp-write';
5
5
 
6
6
  const ACTIVE_CLASS = 'eos-objects-surface';
7
- const safe = fn => { try { return fn(); } catch (_) { return undefined; } };
7
+ const DIALOG_CLASS = 'eos-dp-write-backdrop';
8
8
  const busyIds = new Set();
9
9
 
10
+ const safe = fn => { try { return fn(); } catch (_) { return undefined; } };
11
+ const delay = ms => new Promise(resolve => window.setTimeout(resolve, ms));
12
+
10
13
  const isObjectsRoute = () => {
11
14
  const hash = String(window.location.hash || '');
12
15
  if (/#\/?tab-objects\b|#tab-objects\b/.test(hash)) return true;
@@ -20,133 +23,86 @@
20
23
  return active;
21
24
  };
22
25
 
23
- const interactiveSelector = [
26
+ const valueCellSelector = [
27
+ '.eos-object-value-cell',
28
+ '[data-eos-object-value-cell]',
29
+ '[title*="Wert klicken"]',
30
+ '[title*="Wert schreiben"]',
31
+ '[title*="Write value"]'
32
+ ].join(',');
33
+
34
+ const interactiveIgnoreSelector = [
35
+ `.${DIALOG_CLASS}`,
24
36
  'button',
25
37
  '[role="button"]',
26
38
  'a[href]',
27
39
  'input',
28
40
  'select',
29
41
  'textarea',
30
- '.MuiButton-root',
31
42
  '.MuiIconButton-root',
32
43
  '.MuiCheckbox-root',
33
- '.MuiSwitch-root',
34
- '.MuiSwitch-switchBase',
35
44
  '.MuiMenuItem-root',
36
45
  '.MuiAutocomplete-option',
37
- '.MuiSelect-select',
38
- '.admin-button',
39
46
  '.copyButton'
40
47
  ].join(',');
41
48
 
42
- const nativeLayerSelector = [
43
- '.MuiDialog-root',
44
- '.MuiModal-root',
45
- '.MuiPopover-root',
46
- '.MuiPopper-root',
47
- '.MuiTooltip-popper',
48
- '[role="dialog"]',
49
- '[role="listbox"]',
50
- '[role="menu"]',
51
- '[role="tooltip"]'
52
- ].join(',');
53
-
54
- const valueCellSelector = [
55
- '.eos-object-value-cell',
56
- '[data-eos-object-value-cell]',
57
- '[title*="Wert schreiben"]',
58
- '[title*="Wert klicken"]',
59
- '[title*="Button sofort testen"]'
60
- ].join(',');
61
-
62
- const collect = (base, selector) => {
63
- const out = [];
64
- if (!base) return out;
65
- if (base.nodeType === Node.ELEMENT_NODE && base.matches?.(selector)) out.push(base);
66
- if (base.querySelectorAll) out.push(...Array.from(base.querySelectorAll(selector)));
67
- return out;
68
- };
69
-
70
- const getSocket = () => window.NEXOWATT_EOS_OBJECTS_SOCKET || window.NEXOWATT_EOS_SOCKET || window.socket || null;
71
-
72
49
  const normalizeText = value => String(value ?? '').replace(/\s+/g, ' ').trim();
73
50
 
74
- const isWritableCell = cell => {
75
- if (!cell) return false;
76
- // v51: the React ObjectBrowser now marks every state value cell as writable.
77
- // Do not treat common.write=false as an EOS-side lock; the socket/ACL layer is
78
- // the only authority that may reject a write.
79
- if (cell.getAttribute('data-eos-object-writable') === '1') return true;
80
- if (cell.hasAttribute('data-eos-object-value-cell') && cell.getAttribute('data-eos-object-writable') !== '0') return true;
81
- const title = `${cell.getAttribute('title') || ''} ${cell.textContent || ''}`;
82
- return /wert\s*(klicken|schreiben)|write\s*value|sofort\s*testen/i.test(title);
51
+ const getSocket = () =>
52
+ window.NEXOWATT_EOS_OBJECTS_SOCKET ||
53
+ window.NEXOWATT_EOS_SOCKET ||
54
+ window.socket ||
55
+ null;
56
+
57
+ const waitForSocket = async () => {
58
+ for (let i = 0; i < 40; i += 1) {
59
+ const socket = getSocket();
60
+ if (socket && (typeof socket.sendTo === 'function' || typeof socket.setState === 'function')) return socket;
61
+ await delay(50);
62
+ }
63
+ throw new Error('Socket ist noch nicht bereit');
83
64
  };
84
65
 
85
- const isStateValueCandidate = cell => !!cell && (
86
- cell.hasAttribute?.('data-eos-object-value-cell') ||
87
- cell.classList?.contains('eos-object-value-cell') ||
88
- isWritableCell(cell)
89
- );
90
-
91
- const releaseNativeControls = root => safe(() => {
92
- if (!setSurfaceState()) return;
93
- const base = root && root.nodeType ? root : document;
94
- collect(base, `${interactiveSelector},${nativeLayerSelector}`).forEach(el => {
95
- if (!el || el.closest?.('#eos-assist-root,.eos-assist-root')) return;
96
- el.classList?.remove('eos-security-hidden-delete', 'eos-protected-delete-control', 'eos-disabled-by-security');
97
- el.removeAttribute?.('data-eos-security-blocked');
98
- el.removeAttribute?.('aria-disabled');
99
- if (el.style?.pointerEvents === 'none') el.style.pointerEvents = 'auto';
100
- if (el.style?.visibility === 'hidden') el.style.visibility = 'visible';
101
- if (el.style?.display === 'none' && !el.classList?.contains('eos-native-logout-hidden')) el.style.display = '';
102
- });
103
- });
104
-
105
- const annotateValueCells = root => safe(() => {
106
- if (!setSurfaceState()) return;
107
- const base = root && root.nodeType ? root : document;
108
- collect(base, valueCellSelector).forEach(cell => {
109
- const id = cell.getAttribute('data-eos-object-value-cell') || cell.closest?.('[id]')?.getAttribute('id') || '';
110
- const writable = isWritableCell(cell);
111
- const visibleValue = normalizeText(cell.textContent || '') || '(leer)';
112
- const cache = `${id}|${writable ? 1 : 0}|${visibleValue}`;
113
-
114
- cell.style.pointerEvents = 'auto';
115
- cell.style.cursor = writable ? 'pointer' : 'default';
116
- cell.classList.add('eos-object-value-cell');
117
- cell.classList.toggle('eos-object-value-writable', writable);
118
- cell.classList.toggle('eos-object-value-readonly', !writable);
119
- if (writable) cell.setAttribute('tabindex', '0');
120
-
121
- if (cell.getAttribute('data-eos-value-cache') !== cache) {
122
- cell.setAttribute('data-eos-value-cache', cache);
123
- cell.setAttribute('data-eos-current-visible-value', visibleValue);
124
- const nextTitle = [
125
- id ? `ID: ${id}` : null,
126
- `Aktueller Wert: ${visibleValue}`,
127
- `Status: ${writable ? 'beschreibbar' : 'nur lesbar'}`,
128
- writable ? 'Klicken: Wert schreiben' : 'Nur lesbarer Wert'
129
- ].filter(Boolean).join('\n');
130
- if (cell.getAttribute('title') !== nextTitle) cell.setAttribute('title', nextTitle);
131
- }
132
- });
133
- });
66
+ const withTimeout = async (promise, ms, fallback = null) => {
67
+ let timer;
68
+ try {
69
+ return await Promise.race([
70
+ Promise.resolve(promise),
71
+ new Promise(resolve => { timer = window.setTimeout(() => resolve(fallback), ms); }),
72
+ ]);
73
+ } finally {
74
+ if (timer) window.clearTimeout(timer);
75
+ }
76
+ };
134
77
 
135
78
  const parseBoolean = value => {
136
79
  if (value === true || value === 1) return true;
137
80
  if (value === false || value === 0 || value == null) return false;
138
81
  const normalized = String(value).trim().toLowerCase();
139
- return normalized === 'true' || normalized === '1' || normalized === 'on' || normalized === 'yes' || normalized === 'ja';
82
+ return normalized === 'true' || normalized === '1' || normalized === 'on' || normalized === 'yes' || normalized === 'ja' || normalized === 'ein';
140
83
  };
141
84
 
142
85
  const isNullToken = value => /^\s*(\(null\)|null|undefined)?\s*$/i.test(String(value ?? ''));
143
86
 
87
+ const stripUnit = value => String(value ?? '')
88
+ .replace(/^\s*\((null|undefined)\)\s*$/i, '$1')
89
+ .replace(/\s+(?:%|°C|°F|K|W|kW|Wh|kWh|VA|VAr|A|V|Hz|bar|mbar|Pa|hPa|l|L|m³|m3|s|min|h|d)$/i, '')
90
+ .trim();
91
+
92
+ const inferTypeFromValue = value => {
93
+ const text = stripUnit(value);
94
+ if (/^(true|false|0|1|on|off|yes|no|ja|nein|ein|aus)$/i.test(text)) return 'boolean';
95
+ if (/^-?\d+(?:[.,]\d+)?$/.test(text)) return 'number';
96
+ if (/^\s*[\[{]/.test(String(value || ''))) return 'json';
97
+ return 'string';
98
+ };
99
+
144
100
  const coerceValue = (rawValue, type) => {
145
101
  const raw = String(rawValue ?? '');
146
102
  if (isNullToken(raw) && type !== 'string' && type !== 'json') return null;
147
103
  if (type === 'boolean') return parseBoolean(raw);
148
104
  if (type === 'number') {
149
- const normalized = raw.replace(',', '.').trim();
105
+ const normalized = stripUnit(raw).replace(',', '.');
150
106
  const number = Number(normalized);
151
107
  if (!Number.isFinite(number)) throw new Error(`Ungültige Zahl: ${raw}`);
152
108
  return number;
@@ -166,14 +122,6 @@
166
122
  return String(value);
167
123
  };
168
124
 
169
- const hasNativeWriteDialog = () => {
170
- const dialogs = Array.from(document.querySelectorAll('.MuiDialog-root,[role="dialog"]'));
171
- return dialogs.some(dialog => /write value|wert schreiben|set value|wert setzen/i.test(dialog.textContent || ''));
172
- };
173
-
174
-
175
- let cachedEosAdminInstance = null;
176
-
177
125
  const formatWriteError = error => {
178
126
  if (!error) return 'unbekannter Fehler';
179
127
  if (typeof error === 'string') return error;
@@ -181,15 +129,14 @@
181
129
  try { return JSON.stringify(error); } catch (_) { return String(error); }
182
130
  };
183
131
 
132
+ let cachedEosAdminInstance = null;
184
133
  const normalizeInstanceId = value => {
185
134
  if (!value) return null;
186
135
  if (typeof value === 'string') {
187
136
  const id = value.replace(/^system\.adapter\./, '').trim();
188
137
  return /^eos-admin\.\d+$/.test(id) ? id : null;
189
138
  }
190
- if (typeof value === 'object') {
191
- return normalizeInstanceId(value._id || value.id || value.instance || value.namespace);
192
- }
139
+ if (typeof value === 'object') return normalizeInstanceId(value._id || value.id || value.instance || value.namespace);
193
140
  return null;
194
141
  };
195
142
 
@@ -197,28 +144,21 @@
197
144
  if (cachedEosAdminInstance) return cachedEosAdminInstance;
198
145
  const explicit = normalizeInstanceId(window.NEXOWATT_EOS_ADMIN_INSTANCE || window.adminInstance || window.adapterInstance);
199
146
  if (explicit) return (cachedEosAdminInstance = explicit);
200
-
201
147
  if (socket && typeof socket.getCurrentInstance === 'function') {
202
148
  try {
203
- const current = normalizeInstanceId(await socket.getCurrentInstance());
149
+ const current = normalizeInstanceId(await withTimeout(socket.getCurrentInstance(), 700, null));
204
150
  if (current) return (cachedEosAdminInstance = current);
205
- } catch (_) {
206
- // ignore and try instance list/fallback
207
- }
151
+ } catch (_) { /* fallback below */ }
208
152
  }
209
-
210
153
  if (socket && typeof socket.getAdapterInstances === 'function') {
211
154
  try {
212
- const instances = await socket.getAdapterInstances('eos-admin');
155
+ const instances = await withTimeout(socket.getAdapterInstances('eos-admin'), 900, []);
213
156
  const list = Array.isArray(instances) ? instances : [];
214
157
  const alive = list.find(instance => normalizeInstanceId(instance) && instance?.common?.enabled !== false);
215
158
  const id = normalizeInstanceId(alive || list[0]);
216
159
  if (id) return (cachedEosAdminInstance = id);
217
- } catch (_) {
218
- // ignore and use default
219
- }
160
+ } catch (_) { /* fallback below */ }
220
161
  }
221
-
222
162
  return (cachedEosAdminInstance = 'eos-admin.0');
223
163
  };
224
164
 
@@ -228,48 +168,48 @@
228
168
 
229
169
  if (socket && typeof socket.sendTo === 'function') {
230
170
  const result = socket.sendTo(instance, command, message);
231
- if (result && typeof result.then === 'function') return result;
171
+ if (result && typeof result.then === 'function') return await withTimeout(result, 7000, { ok: false, error: 'sendTo Timeout' });
232
172
  if (result !== undefined) return result;
233
173
  }
234
174
 
235
175
  const rawSocket = socket?._socket || socket?.socket || window.socket;
236
176
  if (rawSocket && typeof rawSocket.emit === 'function') {
237
- return await new Promise((resolve, reject) => {
177
+ return await withTimeout(new Promise((resolve, reject) => {
238
178
  try {
239
179
  rawSocket.emit('sendTo', instance, command, message, response => resolve(response));
240
180
  } catch (error) {
241
181
  reject(error);
242
182
  }
243
- });
183
+ }), 7000, { ok: false, error: 'raw sendTo Timeout' });
244
184
  }
245
185
 
246
186
  throw new Error('socket.sendTo ist nicht verfügbar');
247
187
  };
248
188
 
249
189
  const writeStateUnrestricted = async (socket, id, state) => {
250
- let directError = null;
251
190
  if (!socket) throw new Error('Socket ist nicht bereit');
252
191
 
253
- if (typeof socket.setState === 'function') {
254
- try {
255
- await socket.setState(id, state);
256
- return { ok: true, via: 'socket' };
257
- } catch (error) {
258
- directError = error;
259
- }
260
- } else {
261
- directError = new Error('socket.setState ist nicht verfügbar');
262
- }
263
-
192
+ let backendError = null;
264
193
  try {
265
194
  const response = await callEosAdmin(socket, 'eos:writeState', { id, state });
266
195
  if (response === 'permissionError') throw new Error('sendTo permissionError');
267
196
  if (response && response.ok === false) throw new Error(response.error || 'EOS Backend hat das Schreiben abgelehnt');
268
197
  if (response && response.error) throw new Error(response.error);
269
198
  return { ok: true, via: 'eos-admin' };
270
- } catch (backendError) {
271
- throw new Error(`Direkt: ${formatWriteError(directError)}\nEOS-Backend: ${formatWriteError(backendError)}`);
199
+ } catch (error) {
200
+ backendError = error;
272
201
  }
202
+
203
+ if (typeof socket.setState === 'function') {
204
+ try {
205
+ await withTimeout(socket.setState(id, state), 7000);
206
+ return { ok: true, via: 'socket' };
207
+ } catch (directError) {
208
+ throw new Error(`EOS-Backend: ${formatWriteError(backendError)}\nDirekt: ${formatWriteError(directError)}`);
209
+ }
210
+ }
211
+
212
+ throw new Error(`EOS-Backend: ${formatWriteError(backendError)}\nDirekt: socket.setState ist nicht verfügbar`);
273
213
  };
274
214
 
275
215
  window.NEXOWATT_EOS_WRITE_STATE_UNRESTRICTED = writeStateUnrestricted;
@@ -279,60 +219,90 @@
279
219
  const style = document.createElement('style');
280
220
  style.id = 'eos-dp-write-style';
281
221
  style.textContent = `
282
- .eos-dp-write-backdrop{position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;padding:18px;font-family:Roboto,Arial,sans-serif}.eos-dp-write-dialog{width:min(620px,calc(100vw - 36px));max-height:calc(100vh - 36px);overflow:auto;background:#061827;color:#fff;border:1px solid rgba(0,255,180,.45);border-radius:16px;box-shadow:0 18px 60px rgba(0,0,0,.65),0 0 34px rgba(0,255,180,.22);padding:18px}.eos-dp-write-dialog h3{margin:0 0 8px;font-size:19px;color:#18f4c0}.eos-dp-write-id{font-family:monospace;font-size:12px;opacity:.82;word-break:break-all;margin-bottom:12px}.eos-dp-write-meta{font-size:12px;opacity:.76;margin-bottom:12px}.eos-dp-write-dialog label{display:block;font-size:13px;margin:12px 0 6px}.eos-dp-write-dialog input[type=text],.eos-dp-write-dialog textarea,.eos-dp-write-dialog select{box-sizing:border-box;width:100%;background:#07111d;color:#fff;border:1px solid rgba(255,255,255,.28);border-radius:8px;padding:10px;font-size:15px;outline:none}.eos-dp-write-dialog textarea{min-height:130px;font-family:monospace}.eos-dp-write-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}.eos-dp-write-actions button{border:0;border-radius:10px;padding:9px 16px;font-weight:700;cursor:pointer}.eos-dp-write-primary{background:#00d6a1;color:#03130f}.eos-dp-write-secondary{background:#273746;color:#fff}.eos-dp-write-actions button:disabled{opacity:.55;cursor:wait}.eos-dp-write-error{color:#ff7676;margin-top:10px;font-size:13px;white-space:pre-wrap}.eos-object-value-cell.eos-dp-write-pulse{outline:2px solid #00d6a1;outline-offset:-2px}`;
222
+ html.${ACTIVE_CLASS} .eos-object-value-cell,[data-eos-object-value-cell]{pointer-events:auto!important;cursor:pointer!important}
223
+ .eos-dp-write-backdrop{position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;padding:18px;font-family:Roboto,Arial,sans-serif}.eos-dp-write-dialog{width:min(640px,calc(100vw - 36px));max-height:calc(100vh - 36px);overflow:auto;background:#061827;color:#fff;border:1px solid rgba(0,255,180,.45);border-radius:16px;box-shadow:0 18px 60px rgba(0,0,0,.65),0 0 34px rgba(0,255,180,.22);padding:18px}.eos-dp-write-dialog h3{margin:0 0 8px;font-size:19px;color:#18f4c0}.eos-dp-write-id{font-family:monospace;font-size:12px;opacity:.82;word-break:break-all;margin-bottom:12px}.eos-dp-write-meta{font-size:12px;opacity:.76;margin-bottom:12px}.eos-dp-write-dialog label{display:block;font-size:13px;margin:12px 0 6px}.eos-dp-write-dialog input[type=text],.eos-dp-write-dialog textarea,.eos-dp-write-dialog select{box-sizing:border-box;width:100%;background:#07111d;color:#fff;border:1px solid rgba(255,255,255,.28);border-radius:8px;padding:10px;font-size:15px;outline:none}.eos-dp-write-dialog textarea{min-height:130px;font-family:monospace}.eos-dp-write-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:18px;flex-wrap:wrap}.eos-dp-write-actions button{border:0;border-radius:10px;padding:9px 16px;font-weight:700;cursor:pointer}.eos-dp-write-primary{background:#00d6a1;color:#03130f}.eos-dp-write-secondary{background:#273746;color:#fff}.eos-dp-write-actions button:disabled{opacity:.55;cursor:wait}.eos-dp-write-error{color:#ff7676;margin-top:10px;font-size:13px;white-space:pre-wrap}.eos-dp-write-hint{font-size:12px;opacity:.72;margin-top:8px}.eos-dp-write-toast{position:fixed;right:18px;bottom:84px;z-index:9050;background:#05251e;border:1px solid rgba(0,255,180,.45);box-shadow:0 8px 28px rgba(0,0,0,.45);color:#fff;border-radius:12px;padding:10px 14px;font:13px Roboto,Arial,sans-serif}.eos-object-value-cell.eos-dp-write-pulse{outline:2px solid #00d6a1;outline-offset:-2px}`;
283
224
  document.head.appendChild(style);
284
225
  };
285
226
 
286
- const closeExistingDialog = () => document.querySelector('.eos-dp-write-backdrop')?.remove();
227
+ const showToast = (message, ms = 1800) => {
228
+ injectDialogStyle();
229
+ document.querySelectorAll('.eos-dp-write-toast').forEach(el => el.remove());
230
+ const toast = document.createElement('div');
231
+ toast.className = 'eos-dp-write-toast';
232
+ toast.textContent = message;
233
+ document.body.appendChild(toast);
234
+ window.setTimeout(() => toast.remove(), ms);
235
+ };
287
236
 
288
- const openWriteDialog = async (cell, event) => {
289
- const id = cell?.getAttribute('data-eos-object-value-cell') || cell?.closest?.('[id]')?.id || '';
290
- if (!id || busyIds.has(id)) return;
291
- const socket = getSocket();
292
- if (!socket || typeof socket.setState !== 'function') {
293
- window.alert('EOS: Socket für Datenpunkt-Schreiben ist noch nicht bereit. Bitte Seite einmal hart neu laden (Strg+F5).');
294
- return;
237
+ const closeExistingDialog = () => document.querySelector(`.${DIALOG_CLASS}`)?.remove();
238
+
239
+ const resolveIdFromCell = cell => {
240
+ if (!cell) return '';
241
+ const direct = cell.getAttribute?.('data-eos-object-value-cell') || cell.closest?.('[data-eos-object-value-cell]')?.getAttribute('data-eos-object-value-cell');
242
+ if (direct) return direct.trim();
243
+ const row = cell.closest?.('[data-eos-object-id],[data-id],[data-nodeid],[role="treeitem"],[role="row"],tr,li');
244
+ const attrs = ['data-eos-object-id', 'data-id', 'data-nodeid', 'id'];
245
+ for (const attr of attrs) {
246
+ const value = row?.getAttribute?.(attr);
247
+ if (value && value.includes('.')) return value.replace(/^.*?(?=(?:system|alias|enum|javascript|mqtt|modbus|e3dc|0_userdata|nexowatt|ems|hm-rpc|zigbee|shelly|sonoff|mihome|knx|fhem|sql|influxdb)\.)/, '').trim();
295
248
  }
249
+ return '';
250
+ };
251
+
252
+ const readObjectAndState = async (socket, id, visibleValue) => {
253
+ const objectPromise = typeof socket.getObject === 'function' ? socket.getObject(id) : Promise.resolve(null);
254
+ const statePromise = typeof socket.getState === 'function' ? socket.getState(id) : Promise.resolve(null);
255
+ const [objResult, stateResult] = await Promise.allSettled([
256
+ withTimeout(objectPromise, 1800, null),
257
+ withTimeout(statePromise, 1800, null),
258
+ ]);
259
+ const obj = objResult.status === 'fulfilled' ? objResult.value : null;
260
+ const state = stateResult.status === 'fulfilled' ? stateResult.value : null;
261
+ const common = obj?.common || {};
262
+ const current = state && Object.prototype.hasOwnProperty.call(state, 'val') ? state.val : stripUnit(visibleValue);
263
+ let type = common.type || typeof current;
264
+ if (!type || type === 'undefined' || type === 'object') type = type === 'object' ? 'json' : inferTypeFromValue(current);
265
+ if (type === 'array') type = 'json';
266
+ const role = String(common.role || '');
267
+ const states = common.states && typeof common.states === 'object' ? common.states : null;
268
+ return { obj, state, common, current, type, role, states };
269
+ };
296
270
 
271
+ const openWriteDialog = async (cell, event) => {
272
+ const id = resolveIdFromCell(cell);
273
+ if (!id || busyIds.has(id)) return;
297
274
  busyIds.add(id);
298
- cell?.classList.add('eos-dp-write-pulse');
299
- window.setTimeout(() => cell?.classList.remove('eos-dp-write-pulse'), 900);
275
+ injectDialogStyle();
276
+ cell?.classList?.add('eos-dp-write-pulse');
277
+ window.setTimeout(() => cell?.classList?.remove('eos-dp-write-pulse'), 650);
300
278
 
301
279
  try {
302
- const [objResult, stateResult] = await Promise.allSettled([
303
- typeof socket.getObject === 'function' ? socket.getObject(id) : Promise.resolve(null),
304
- typeof socket.getState === 'function' ? socket.getState(id) : Promise.resolve(null),
305
- ]);
306
- const obj = objResult.status === 'fulfilled' ? objResult.value : null;
307
- const state = stateResult.status === 'fulfilled' ? stateResult.value : null;
308
- if (!obj || obj.type !== 'state') {
280
+ const socket = await waitForSocket();
281
+ const visibleValue = normalizeText(cell?.textContent || '');
282
+ const meta = await readObjectAndState(socket, id, visibleValue);
283
+ const { obj, common, current, type, role, states } = meta;
284
+
285
+ if (obj && obj.type && obj.type !== 'state') {
286
+ showToast(`EOS: "${id}" ist kein State (${obj.type})`);
309
287
  busyIds.delete(id);
310
288
  return;
311
289
  }
312
- const common = obj?.common || {};
313
290
  if (common.type === 'file') {
291
+ showToast('EOS: Datei-Datenpunkte werden nicht als normaler State-Wert geschrieben.');
314
292
  busyIds.delete(id);
315
- window.alert('EOS: Datei-Datenpunkte werden über die Datei-/Objektansicht geöffnet und nicht als State-Wert geschrieben.');
316
293
  return;
317
294
  }
318
- const role = String(common.role || '');
319
- let type = common.type || typeof state?.val;
320
- if (!type || type === 'undefined') type = 'string';
321
- if (type === 'object') type = 'json';
322
- const current = state?.val;
323
- const states = common.states && typeof common.states === 'object' ? common.states : null;
324
-
325
- if (/^button/.test(role)) {
295
+
296
+ if (/^button/.test(role) && (type === 'boolean' || type === 'string' || !type)) {
326
297
  await writeStateUnrestricted(socket, id, { val: true, ack: false, q: 0 });
298
+ showToast(`Befehl geschrieben: ${id}`);
327
299
  busyIds.delete(id);
328
300
  return;
329
301
  }
330
302
 
331
- injectDialogStyle();
332
303
  closeExistingDialog();
333
-
334
304
  const backdrop = document.createElement('div');
335
- backdrop.className = 'eos-dp-write-backdrop';
305
+ backdrop.className = DIALOG_CLASS;
336
306
  const dialog = document.createElement('div');
337
307
  dialog.className = 'eos-dp-write-dialog';
338
308
  dialog.setAttribute('role', 'dialog');
@@ -344,13 +314,14 @@
344
314
  <label class="eos-dp-write-label">Neuer Wert</label>
345
315
  <div class="eos-dp-write-input-wrap"></div>
346
316
  <label style="display:flex;align-items:center;gap:8px;margin-top:12px"><input class="eos-dp-write-ack" type="checkbox"> ack=true schreiben</label>
317
+ <div class="eos-dp-write-hint">EOS schreibt direkt über die Backend-Bridge. common.write/read blockiert hier nicht mehr.</div>
347
318
  <div class="eos-dp-write-error" hidden></div>
348
319
  <div class="eos-dp-write-actions"><button class="eos-dp-write-secondary" type="button">Abbrechen</button><button class="eos-dp-write-primary" type="button">Schreiben</button></div>`;
349
320
  backdrop.appendChild(dialog);
350
321
  document.body.appendChild(backdrop);
351
322
 
352
323
  dialog.querySelector('.eos-dp-write-id').textContent = id;
353
- dialog.querySelector('.eos-dp-write-meta').textContent = `Typ: ${type}${role ? ` · Rolle: ${role}` : ''}${common.unit ? ` · Einheit: ${common.unit}` : ''}`;
324
+ dialog.querySelector('.eos-dp-write-meta').textContent = `Typ: ${type}${role ? ` · Rolle: ${role}` : ''}${common.unit ? ` · Einheit: ${common.unit}` : ''}${obj ? '' : ' · Objekt lädt langsam / Fallback aktiv'}`;
354
325
 
355
326
  const inputWrap = dialog.querySelector('.eos-dp-write-input-wrap');
356
327
  let input;
@@ -365,8 +336,8 @@
365
336
  input.value = current == null ? Object.keys(states)[0] || '' : String(current);
366
337
  } else if (type === 'boolean') {
367
338
  input = document.createElement('select');
368
- input.innerHTML = '<option value="true">true</option><option value="false">false</option>';
369
- input.value = parseBoolean(current) ? 'true' : 'false';
339
+ input.innerHTML = '<option value="true">true</option><option value="false">false</option><option value="null">null</option>';
340
+ input.value = current == null ? 'null' : (parseBoolean(current) ? 'true' : 'false');
370
341
  } else if (type === 'json') {
371
342
  input = document.createElement('textarea');
372
343
  input.value = stateToString(current);
@@ -393,13 +364,13 @@
393
364
  writeBtn.disabled = true;
394
365
  cancelBtn.disabled = true;
395
366
  try {
396
- let value = input.value;
397
367
  const effectiveType = states ? (common.type || type) : type;
398
- value = coerceValue(value, effectiveType);
368
+ const value = coerceValue(input.value, effectiveType);
399
369
  await writeStateUnrestricted(socket, id, { val: value, ack: !!ackInput.checked, q: 0 });
400
370
  close();
371
+ showToast(`Geschrieben: ${id}`);
401
372
  } catch (error) {
402
- errorEl.textContent = `Schreiben fehlgeschlagen: ${error?.message || error}`;
373
+ errorEl.textContent = `Schreiben fehlgeschlagen:\n${error?.message || error}`;
403
374
  errorEl.hidden = false;
404
375
  writeBtn.disabled = false;
405
376
  cancelBtn.disabled = false;
@@ -419,102 +390,56 @@
419
390
  window.setTimeout(() => input.focus(), 30);
420
391
  } catch (error) {
421
392
  busyIds.delete(id);
422
- window.alert(`EOS: Datenpunkt konnte nicht geschrieben werden: ${error?.message || error}`);
393
+ window.alert(`EOS: Datenpunkt konnte nicht geschrieben werden:\n${error?.message || error}`);
423
394
  }
424
395
  };
425
396
 
426
- const run = root => {
397
+ const handleClickCapture = event => {
427
398
  if (!setSurfaceState()) return;
428
- releaseNativeControls(root);
429
- annotateValueCells(root);
430
- };
431
-
432
- let scheduled = false;
433
- const pendingRoots = new Set();
434
- const schedule = root => {
435
- if (!setSurfaceState()) return;
436
- if (root) pendingRoots.add(root);
437
- if (scheduled) return;
438
- scheduled = true;
439
- const execute = () => {
440
- scheduled = false;
441
- const roots = pendingRoots.size ? Array.from(pendingRoots) : [document];
442
- pendingRoots.clear();
443
- roots.slice(0, 80).forEach(run);
444
- };
445
- if ('requestIdleCallback' in window) window.requestIdleCallback(execute, { timeout: 500 });
446
- else window.requestAnimationFrame(execute);
447
- };
448
-
449
- const handlePointerAssist = event => {
450
- if (!setSurfaceState()) return;
451
- const cell = event.target?.closest?.('.eos-object-value-cell');
452
- if (isWritableCell(cell)) cell.classList.add('eos-object-value-hover');
453
- };
454
-
455
- const handlePointerLeave = event => {
456
- event.target?.closest?.('.eos-object-value-cell')?.classList.remove('eos-object-value-hover');
457
- };
458
-
459
- const shouldSkipFallback = cell => !!cell?.querySelector?.('.MuiSwitch-root,.admin-button,button,.copyButton');
460
-
461
- const scheduleFallbackWriter = event => {
462
- if (!setSurfaceState()) return;
463
- const cell = event.target?.closest?.('.eos-object-value-cell,[data-eos-object-value-cell]');
464
- if (!cell || !isStateValueCandidate(cell) || shouldSkipFallback(cell)) return;
465
- const id = cell.getAttribute('data-eos-object-value-cell') || '';
399
+ if (event.defaultPrevented || event.button !== 0) return;
400
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
401
+ if (event.target?.closest?.(`.${DIALOG_CLASS}`)) return;
402
+ if (event.target?.closest?.(interactiveIgnoreSelector) && !event.target?.closest?.(valueCellSelector)) return;
403
+ const cell = event.target?.closest?.(valueCellSelector);
404
+ if (!cell) return;
405
+ const id = resolveIdFromCell(cell);
466
406
  if (!id) return;
467
- window.setTimeout(() => {
468
- if (!setSurfaceState()) return;
469
- if (!cell.isConnected || hasNativeWriteDialog()) return;
470
- openWriteDialog(cell, event);
471
- }, 180);
407
+ event.preventDefault();
408
+ event.stopPropagation();
409
+ event.stopImmediatePropagation?.();
410
+ openWriteDialog(cell, event);
472
411
  };
473
412
 
474
- window.addEventListener('hashchange', () => window.setTimeout(() => schedule(document), 30));
475
- window.addEventListener('popstate', () => window.setTimeout(() => schedule(document), 30));
476
- document.addEventListener('mouseover', handlePointerAssist, true);
477
- document.addEventListener('mouseout', handlePointerLeave, true);
478
- document.addEventListener('click', event => {
479
- if (!setSurfaceState()) return;
480
- const cell = event.target?.closest?.('.eos-object-value-cell,[data-eos-object-value-cell]');
481
- if (cell) {
482
- releaseNativeControls(cell);
483
- scheduleFallbackWriter(event);
484
- }
485
- }, true);
486
- document.addEventListener('click', event => {
487
- if (!setSurfaceState()) return;
488
- const nativeInteractive = event.target?.closest?.(`${interactiveSelector},${nativeLayerSelector},.eos-object-value-cell,[data-eos-object-value-cell]`);
489
- if (nativeInteractive) {
490
- releaseNativeControls(nativeInteractive);
491
- scheduleFallbackWriter(event);
492
- window.setTimeout(() => schedule(nativeInteractive), 80);
493
- }
494
- }, false);
495
- document.addEventListener('keydown', event => {
496
- if (!setSurfaceState()) return;
497
- if (event.key !== 'Enter') return;
498
- const cell = event.target?.closest?.('.eos-object-value-cell,[data-eos-object-value-cell]');
499
- if (!cell || !isStateValueCandidate(cell) || shouldSkipFallback(cell)) return;
413
+ const handleKeydownCapture = event => {
414
+ if (!setSurfaceState() || event.key !== 'Enter') return;
415
+ const cell = event.target?.closest?.(valueCellSelector);
416
+ if (!cell) return;
417
+ const id = resolveIdFromCell(cell);
418
+ if (!id) return;
500
419
  event.preventDefault();
420
+ event.stopPropagation();
421
+ event.stopImmediatePropagation?.();
501
422
  openWriteDialog(cell, event);
502
- }, true);
423
+ };
503
424
 
504
- const observer = new MutationObserver(records => {
425
+ const handlePointerOver = event => {
505
426
  if (!setSurfaceState()) return;
506
- for (const rec of records) {
507
- rec.addedNodes?.forEach(node => {
508
- if (node.nodeType === Node.ELEMENT_NODE) schedule(node);
509
- });
427
+ const cell = event.target?.closest?.(valueCellSelector);
428
+ if (cell && resolveIdFromCell(cell)) {
429
+ cell.classList.add('eos-object-value-cell');
430
+ cell.setAttribute('tabindex', '0');
431
+ if (!cell.getAttribute('title')) cell.setAttribute('title', 'Klicken: Wert schreiben');
510
432
  }
511
- });
433
+ };
512
434
 
513
435
  const start = () => {
514
436
  setSurfaceState();
515
- schedule(document);
516
- observer.observe(document.documentElement, { childList: true, subtree: true });
517
- window.setInterval(() => schedule(document), 60000);
437
+ injectDialogStyle();
438
+ document.addEventListener('click', handleClickCapture, true);
439
+ document.addEventListener('keydown', handleKeydownCapture, true);
440
+ document.addEventListener('pointerover', handlePointerOver, true);
441
+ window.addEventListener('hashchange', setSurfaceState);
442
+ window.addEventListener('popstate', setSurfaceState);
518
443
  };
519
444
 
520
445
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });