iobroker.mywebui 1.37.34 → 1.37.36

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.
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.37.34",
4
+ "version": "1.37.36",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.37.34",
3
+ "version": "1.37.36",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -424,6 +424,59 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
424
424
  const bindRow = document.createElement('div');
425
425
  bindRow.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #ddd;';
426
426
 
427
+ // □ button on the LEFT (like Properties panel style)
428
+ const bindBtn = document.createElement('button');
429
+ bindBtn.textContent = '□';
430
+ bindBtn.title = 'Right-click for binding options';
431
+ bindBtn.style.cssText = existingBinding
432
+ ? 'width:16px;height:16px;padding:0;font-size:11px;line-height:1;border:1px solid #888;background:#ffd700;cursor:pointer;flex-shrink:0;'
433
+ : 'width:16px;height:16px;padding:0;font-size:11px;line-height:1;border:1px solid #888;background:#f0f0f0;cursor:pointer;flex-shrink:0;';
434
+
435
+ // Left-click: open binding editor (pass null so openBindingsEditor doesn't reject on service check)
436
+ bindBtn.onclick = () => {
437
+ const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
438
+ serviceContainer.config.openBindingsEditor(property, [designItem], null, 'property');
439
+ };
440
+
441
+ // Right-click: context menu (like Properties panel)
442
+ bindBtn.oncontextmenu = (e) => {
443
+ e.preventDefault();
444
+ const existing = document.getElementById('__vis-ctx-menu');
445
+ if (existing) existing.remove();
446
+
447
+ const menu = document.createElement('div');
448
+ menu.id = '__vis-ctx-menu';
449
+ menu.style.cssText = `position:fixed;left:${e.clientX}px;top:${e.clientY}px;background:#2d2d2d;color:#ddd;
450
+ font-size:12px;border:1px solid #555;border-radius:3px;z-index:99999;min-width:130px;box-shadow:2px 2px 6px rgba(0,0,0,0.5);`;
451
+
452
+ const addItem = (text, cb, disabled = false) => {
453
+ const item = document.createElement('div');
454
+ item.textContent = text;
455
+ item.style.cssText = `padding:6px 12px;cursor:${disabled ? 'default' : 'pointer'};color:${disabled ? '#666' : '#ddd'};`;
456
+ if (!disabled) {
457
+ item.onmouseenter = () => item.style.background = '#3e6db4';
458
+ item.onmouseleave = () => item.style.background = '';
459
+ item.onclick = () => { menu.remove(); cb(); };
460
+ }
461
+ menu.appendChild(item);
462
+ };
463
+
464
+ addItem('edit binding', () => {
465
+ const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
466
+ serviceContainer.config.openBindingsEditor(property, [designItem], null, 'property');
467
+ });
468
+ addItem('clear binding', () => {
469
+ designItem.removeAttribute('bind-prop:hidden');
470
+ this._updateVisibilityPanel();
471
+ }, !existingBinding);
472
+
473
+ document.body.appendChild(menu);
474
+ const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('mousedown', close); } };
475
+ setTimeout(() => document.addEventListener('mousedown', close), 0);
476
+ };
477
+
478
+ bindRow.appendChild(bindBtn);
479
+
427
480
  const bindLabel = document.createElement('span');
428
481
  bindLabel.textContent = 'Visibility';
429
482
  bindLabel.style.cssText = 'font-size:12px;font-weight:600;flex:1;';
@@ -434,34 +487,10 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
434
487
  const statusSpan = document.createElement('span');
435
488
  statusSpan.textContent = sig;
436
489
  statusSpan.title = JSON.stringify(existingBinding);
437
- statusSpan.style.cssText = 'font-size:10px;color:#555;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
490
+ statusSpan.style.cssText = 'font-size:10px;color:#aaa;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;';
438
491
  bindRow.appendChild(statusSpan);
439
492
  }
440
493
 
441
- const bindBtn = document.createElement('button');
442
- bindBtn.textContent = '□';
443
- bindBtn.title = 'Open binding editor';
444
- bindBtn.style.cssText = existingBinding
445
- ? 'width:20px;height:20px;padding:0;font-size:13px;line-height:1;border:1px solid #888;background:#ff0;cursor:pointer;flex-shrink:0;'
446
- : 'width:20px;height:20px;padding:0;font-size:13px;line-height:1;border:1px solid #888;background:#f0f0f0;cursor:pointer;flex-shrink:0;';
447
- bindBtn.onclick = () => {
448
- const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
449
- serviceContainer.config.openBindingsEditor(property, [designItem], existingBinding, 'property');
450
- };
451
- bindRow.appendChild(bindBtn);
452
-
453
- if (existingBinding) {
454
- const clearBtn = document.createElement('button');
455
- clearBtn.textContent = '✕';
456
- clearBtn.title = 'Remove binding';
457
- clearBtn.style.cssText = 'width:20px;height:20px;padding:0;font-size:11px;line-height:1;border:1px solid #c66;background:#fee;cursor:pointer;color:#c00;flex-shrink:0;';
458
- clearBtn.onclick = () => {
459
- designItem.removeAttribute('bind-prop:hidden');
460
- this._updateVisibilityPanel();
461
- };
462
- bindRow.appendChild(clearBtn);
463
- }
464
-
465
494
  content.appendChild(bindRow);
466
495
 
467
496
  // --- GROUP ACCESS CONTROL ---
@@ -1,271 +1,76 @@
1
1
  import { iobrokerHandler } from '../common/IobrokerHandler.js';
2
2
 
3
- /**
4
- * Visibility Service - Handles element visibility based on user groups and conditions
5
- * Provides protection against browser manipulation
6
- */
7
3
  class VisibilityService {
8
4
  static instance = new VisibilityService();
9
-
10
- #intervalChecks = new Map();
11
- #originalDisplayStyles = new WeakMap();
12
- #configs = new WeakMap();
13
- #tampering = false;
14
-
15
- constructor() {
16
- this.#setupAntiTampering();
17
- }
18
-
19
- /**
20
- * Setup anti-tampering detection
21
- */
22
- #setupAntiTampering() {
23
- // Detect DevTools opening
24
- const detectDevTools = () => {
25
- const threshold = 160;
26
- const widthThreshold = window.outerWidth - window.innerWidth > threshold;
27
- const heightThreshold = window.outerHeight - window.innerHeight > threshold;
28
-
29
- if (widthThreshold || heightThreshold) {
30
- if (!this.#tampering) {
31
- this.#tampering = true;
32
- console.warn('⚠️ [Visibility] Developer tools detected - visibility checks will be re-validated');
33
- this.#revalidateAll();
34
- }
35
- } else {
36
- this.#tampering = false;
37
- }
38
- };
39
-
40
- setInterval(detectDevTools, 1000);
41
-
42
- // Detect element.style modifications
43
- this.#setupMutationObserver();
44
- }
45
-
46
- /**
47
- * Setup mutation observer to detect tampering
48
- */
49
- #setupMutationObserver() {
50
- const observer = new MutationObserver((mutations) => {
51
- for (const mutation of mutations) {
52
- const element = mutation.target;
53
-
54
- if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
55
- if (element.hasAttribute('data-visibility-controlled')) {
56
- console.warn('🚨 [Visibility] Style tampered:', element);
57
- this.#revalidateElement(element);
58
- }
59
- }
60
5
 
61
- // data-visibility-controlled silinib — bypass cəhdi
62
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-visibility-controlled') {
63
- if (!element.hasAttribute('data-visibility-controlled') && this.#configs.has(element)) {
64
- console.warn('🚨 [Visibility] Controlled attribute removed — restoring');
65
- element.setAttribute('data-visibility-controlled', 'true');
66
- this.#revalidateElement(element);
67
- }
68
- }
69
- }
70
- });
71
-
72
- observer.observe(document.body, {
73
- attributes: true,
74
- subtree: true,
75
- attributeFilter: ['style', 'data-visibility-controlled']
76
- });
77
- }
78
-
79
- /**
80
- * Apply visibility rules to an element
81
- * @param {HTMLElement} element
82
- * @param {object} visibilityConfig
83
- */
84
- async applyVisibility(element, visibilityConfig) {
85
- if (!visibilityConfig || !visibilityConfig.enabled) {
86
- return;
87
- }
88
-
89
- console.log('🔍 [Visibility] Applying visibility rules to:', element.tagName, visibilityConfig);
90
-
91
- // Store original display style
92
- if (!this.#originalDisplayStyles.has(element)) {
93
- this.#originalDisplayStyles.set(element, element.style.display || '');
94
- }
95
-
96
- // Config-i memory-də saxla — DOM-da sensitiv məlumat saxlama
97
- this.#configs.set(element, visibilityConfig);
98
-
99
- // Yalnız "izlənilir" işarəsi
100
- element.setAttribute('data-visibility-controlled', 'true');
6
+ #originalDisplayStyles = new WeakMap();
101
7
 
102
- // Initial check
103
- await this.#checkAndApply(element, visibilityConfig);
104
-
105
- // Subscribe to datapoint changes if objectId specified
106
- if (visibilityConfig.objectId) {
107
- console.log('📡 [Visibility] Subscribing to state changes:', visibilityConfig.objectId);
108
- await iobrokerHandler.subscribeState(visibilityConfig.objectId, (id, state) => {
109
- console.log('🔔 [Visibility] State changed:', id, 'new value:', state?.val);
110
- this.#checkAndApply(element, visibilityConfig);
111
- });
112
- }
113
-
114
- // Periodic re-validation (protection against tampering)
115
- const intervalId = setInterval(() => {
116
- this.#checkAndApply(element, visibilityConfig);
117
- }, 30000); // Check every 30 seconds (subscription handles real-time updates)
118
-
119
- this.#intervalChecks.set(element, intervalId);
120
- }
121
-
122
- /**
123
- * Check visibility and apply
124
- */
125
8
  async #checkAndApply(element, visibilityConfig) {
126
9
  try {
127
- let result;
10
+ const result = await iobrokerHandler.checkVisibility(visibilityConfig);
128
11
 
129
- if (visibilityConfig.groups && visibilityConfig.groups.length > 0) {
130
- // Qrup yoxlaması → backend-ə sendTo: qrupları backend DB-dən oxuyur, frontend-ə inanmır
131
- const username = await iobrokerHandler.connection.getCurrentUser();
132
- if (username) {
133
- result = await iobrokerHandler.connection.sendTo(
134
- 'mywebui.0',
135
- 'checkVisibility',
136
- { username, allowedGroups: visibilityConfig.groups }
137
- );
138
- result = result ?? { visible: false };
139
- } else {
140
- // User alınmadı → fallback
141
- result = await iobrokerHandler.checkVisibility(visibilityConfig);
142
- }
143
- } else {
144
- // Qrup yoxlaması yoxdur → datapoint condition yoxlaması (frontend)
145
- result = await iobrokerHandler.checkVisibility(visibilityConfig);
146
- }
147
-
148
- console.log('✅ [Visibility] Check result for', element.tagName, ':', { visible: result.visible, enabled: result.enabled });
149
-
150
- // Apply visibility
151
12
  if (!result.visible) {
152
13
  element.style.display = 'none';
153
14
  element.style.visibility = 'hidden';
154
- element.setAttribute('aria-hidden', 'true');
155
15
  } else {
156
- const originalDisplay = this.#originalDisplayStyles.get(element) || '';
157
- element.style.display = originalDisplay;
158
- element.style.visibility = 'visible';
159
- element.removeAttribute('aria-hidden');
16
+ const orig = this.#originalDisplayStyles.get(element) || '';
17
+ element.style.display = orig;
18
+ element.style.visibility = '';
160
19
  }
161
-
162
- // Apply enabled/disabled
20
+
163
21
  if (!result.enabled) {
164
22
  element.style.pointerEvents = 'none';
165
23
  element.style.opacity = '0.5';
166
- element.setAttribute('disabled', 'true');
167
- if (element.setAttribute) {
168
- element.setAttribute('data-disabled-by-visibility', 'true');
169
- }
170
24
  } else {
171
25
  element.style.pointerEvents = '';
172
26
  element.style.opacity = '';
173
- element.removeAttribute('disabled');
174
- element.removeAttribute('data-disabled-by-visibility');
175
27
  }
176
-
177
- // Store encrypted hash to detect tampering
178
- this.#storeElementHash(element, result);
179
- }
180
- catch (err) {
181
- console.error('❌ [Visibility] Failed to check visibility:', err);
28
+ } catch (err) {
29
+ console.error('[Visibility] Check failed:', err);
182
30
  }
183
31
  }
184
-
185
- /**
186
- * Store element hash for tampering detection
187
- */
188
- #storeElementHash(element, result) {
189
- const hash = btoa(JSON.stringify({
190
- visible: result.visible,
191
- enabled: result.enabled,
192
- timestamp: Date.now()
193
- }));
194
- element.setAttribute('data-visibility-hash', hash);
195
- }
196
-
197
- /**
198
- * Validate element hasn't been tampered
199
- */
200
- async #revalidateElement(element) {
201
- try {
202
- // DOM-dan deyil, WeakMap-dan oxu — attribute dəyişdirilsə belə orijinal config qalır
203
- const config = this.#configs.get(element);
204
- if (config?.enabled) {
205
- await this.#checkAndApply(element, config);
206
- }
32
+
33
+ async applyVisibility(element, visibilityConfig) {
34
+ if (!visibilityConfig || !visibilityConfig.enabled) return;
35
+
36
+ if (!this.#originalDisplayStyles.has(element)) {
37
+ this.#originalDisplayStyles.set(element, element.style.display || '');
207
38
  }
208
- catch (err) {
209
- console.error('[Visibility] Revalidation error:', err);
39
+
40
+ await this.#checkAndApply(element, visibilityConfig);
41
+
42
+ if (visibilityConfig.objectId) {
43
+ await iobrokerHandler.subscribeState(visibilityConfig.objectId, () => {
44
+ this.#checkAndApply(element, visibilityConfig);
45
+ });
210
46
  }
211
47
  }
212
-
213
- /**
214
- * Revalidate all controlled elements
215
- */
216
- #revalidateAll() {
217
- const controlled = document.querySelectorAll('[data-visibility-controlled="true"]');
218
- controlled.forEach(element => {
219
- this.#revalidateElement(element);
220
- });
221
- }
222
-
223
- /**
224
- * Remove visibility control from element
225
- */
48
+
226
49
  removeVisibility(element) {
227
- const intervalId = this.#intervalChecks.get(element);
228
- if (intervalId) {
229
- clearInterval(intervalId);
230
- this.#intervalChecks.delete(element);
231
- }
232
-
233
- this.#configs.delete(element);
234
- element.removeAttribute('data-visibility-controlled');
235
- element.removeAttribute('data-visibility-hash');
236
-
237
- // Restore original style
238
50
  const originalDisplay = this.#originalDisplayStyles.get(element);
239
51
  if (originalDisplay !== undefined) {
240
52
  element.style.display = originalDisplay;
53
+ this.#originalDisplayStyles.delete(element);
241
54
  }
242
55
  element.style.visibility = '';
243
56
  element.style.pointerEvents = '';
244
57
  element.style.opacity = '';
245
58
  }
246
-
247
- /**
248
- * Scan and apply visibility to all elements with visibility config
249
- */
59
+
250
60
  async scanAndApply(container = document.body) {
251
61
  const elements = container.querySelectorAll('[data-visibility-enabled="true"]');
252
- console.log(`🔍 [Visibility] Scanning ${elements.length} elements with visibility config`);
253
-
254
62
  for (const element of elements) {
255
63
  try {
256
- // Read config from separate attributes
257
64
  const visibilityConfig = {
258
- enabled: element.getAttribute('data-visibility-enabled') === 'true',
65
+ enabled: true,
259
66
  objectId: element.getAttribute('data-visibility-signal'),
260
67
  condition: element.getAttribute('data-visibility-condition') || '==',
261
68
  conditionValue: element.getAttribute('data-visibility-value') || '',
262
69
  action: element.getAttribute('data-visibility-action') || 'hide',
263
70
  groups: element.getAttribute('data-visibility-groups')?.split(',').filter(g => g) || []
264
71
  };
265
-
266
72
  await this.applyVisibility(element, visibilityConfig);
267
- }
268
- catch (err) {
73
+ } catch (err) {
269
74
  console.error('[Visibility] Error parsing visibility config:', err);
270
75
  }
271
76
  }