iobroker.mywebui 1.37.33 → 1.37.35

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.33",
4
+ "version": "1.37.35",
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.33",
3
+ "version": "1.37.35",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -403,57 +403,176 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
403
403
  const designItem = selectedItems[0];
404
404
  const element = designItem.element;
405
405
 
406
- // Read existing visible binding from bind-visible: attribute
407
- const existingBindingAttr = element.getAttribute('bind-visible:');
406
+ // Read existing binding from bind-prop:hidden attribute
407
+ const bindAttr = element.getAttribute('bind-prop:hidden');
408
408
  let existingBinding = null;
409
- if (existingBindingAttr != null) {
410
- const parsed = bindingsHelper.parseBinding(element, 'bind-visible:', existingBindingAttr, 'visible', 'bind-visible:');
409
+ if (bindAttr != null) {
410
+ const parsed = bindingsHelper.parseBinding(element, 'bind-prop:hidden', bindAttr, 'property', 'bind-prop:');
411
411
  if (parsed) existingBinding = parsed[1];
412
412
  }
413
413
 
414
+ // Read group access control config from data attributes
415
+ const dataConfig = {
416
+ enabled: element.getAttribute('data-visibility-enabled') === 'true',
417
+ groups: element.getAttribute('data-visibility-groups')?.split(',').filter(g => g) || [],
418
+ action: element.getAttribute('data-visibility-action') || 'hide'
419
+ };
420
+
414
421
  content.innerHTML = '';
415
422
 
416
- // Property row: label + current signal + bind button + clear button
417
- const row = document.createElement('div');
418
- row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;';
423
+ // --- BINDING ROW ---
424
+ const bindRow = document.createElement('div');
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
+
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
436
+ bindBtn.onclick = () => {
437
+ const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
438
+ serviceContainer.config.openBindingsEditor(property, [designItem], existingBinding, '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], existingBinding, '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);
419
479
 
420
- const label = document.createElement('span');
421
- label.textContent = 'Visibility';
422
- label.style.cssText = 'font-size:12px;font-weight:600;flex:1;';
423
- row.appendChild(label);
480
+ const bindLabel = document.createElement('span');
481
+ bindLabel.textContent = 'Visibility';
482
+ bindLabel.style.cssText = 'font-size:12px;font-weight:600;flex:1;';
483
+ bindRow.appendChild(bindLabel);
424
484
 
425
485
  if (existingBinding) {
426
- const statusSpan = document.createElement('span');
427
486
  const sig = existingBinding.signal || (existingBinding.expression ? 'expr' : '?');
487
+ const statusSpan = document.createElement('span');
428
488
  statusSpan.textContent = sig;
429
489
  statusSpan.title = JSON.stringify(existingBinding);
430
- statusSpan.style.cssText = 'font-size:10px;color:#555;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
431
- row.appendChild(statusSpan);
490
+ statusSpan.style.cssText = 'font-size:10px;color:#aaa;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;';
491
+ bindRow.appendChild(statusSpan);
432
492
  }
433
493
 
434
- const bindBtn = document.createElement('button');
435
- bindBtn.textContent = '□';
436
- bindBtn.title = 'Open binding editor';
437
- bindBtn.style.cssText = 'width:20px;height:20px;padding:0;font-size:13px;line-height:1;border:1px solid #888;background:#f0f0f0;cursor:pointer;flex-shrink:0;';
438
- bindBtn.onclick = () => {
439
- const property = { name: 'hidden', propertyType: 'visible' };
440
- serviceContainer.config.openBindingsEditor(property, [designItem], existingBinding, 'visible');
494
+ content.appendChild(bindRow);
495
+
496
+ // --- GROUP ACCESS CONTROL ---
497
+ const updateGroupConfig = (key, value) => {
498
+ dataConfig[key] = value;
499
+ if (key === 'enabled') {
500
+ if (value) designItem.setAttribute('data-visibility-enabled', 'true');
501
+ else designItem.removeAttribute('data-visibility-enabled');
502
+ } else if (key === 'groups') {
503
+ if (value && value.length > 0) designItem.setAttribute('data-visibility-groups', value.join(','));
504
+ else designItem.removeAttribute('data-visibility-groups');
505
+ } else if (key === 'action') {
506
+ if (value) designItem.setAttribute('data-visibility-action', value);
507
+ else designItem.removeAttribute('data-visibility-action');
508
+ }
441
509
  };
442
- row.appendChild(bindBtn);
443
510
 
444
- if (existingBinding) {
445
- const clearBtn = document.createElement('button');
446
- clearBtn.textContent = '';
447
- clearBtn.title = 'Remove binding';
448
- 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;';
449
- clearBtn.onclick = () => {
450
- designItem.removeAttribute('bind-visible:');
451
- this._updateVisibilityPanel();
511
+ // Enable checkbox
512
+ const enableDiv = document.createElement('div');
513
+ enableDiv.style.cssText = 'margin-bottom:10px;';
514
+ const enableLabel = document.createElement('label');
515
+ enableLabel.style.cssText = 'display:flex;align-items:center;gap:5px;cursor:pointer;font-size:12px;';
516
+ const enableCheck = document.createElement('input');
517
+ enableCheck.type = 'checkbox';
518
+ enableCheck.checked = dataConfig.enabled || false;
519
+ enableCheck.onchange = () => updateGroupConfig('enabled', enableCheck.checked);
520
+ enableLabel.appendChild(enableCheck);
521
+ enableLabel.appendChild(document.createTextNode('Enable Group Visibility Control'));
522
+ enableDiv.appendChild(enableLabel);
523
+ content.appendChild(enableDiv);
524
+
525
+ // Groups
526
+ const groupsDiv = document.createElement('div');
527
+ groupsDiv.style.cssText = 'margin-bottom:10px;';
528
+ const groupsLabel = document.createElement('label');
529
+ groupsLabel.style.cssText = 'font-size:11px;font-weight:600;display:block;margin-bottom:3px;color:#555;';
530
+ groupsLabel.textContent = 'Only for groups:';
531
+ groupsDiv.appendChild(groupsLabel);
532
+
533
+ const groupsList = document.createElement('div');
534
+ groupsList.style.cssText = 'max-height:100px;overflow-y:auto;border:1px solid #ccc;padding:6px;background:#fff;border-radius:3px;';
535
+
536
+ const userGroups = await iobrokerHandler.getUserGroups();
537
+ const selectedGroups = dataConfig.groups || [];
538
+
539
+ userGroups.forEach(group => {
540
+ const groupLabel = document.createElement('label');
541
+ groupLabel.style.cssText = 'display:flex;align-items:center;gap:5px;font-size:11px;padding:3px;cursor:pointer;';
542
+ const groupCheck = document.createElement('input');
543
+ groupCheck.type = 'checkbox';
544
+ groupCheck.checked = selectedGroups.includes(group.id);
545
+ groupCheck.onchange = () => {
546
+ let groups = [...dataConfig.groups];
547
+ if (groupCheck.checked) {
548
+ if (!groups.includes(group.id)) groups.push(group.id);
549
+ } else {
550
+ groups = groups.filter(g => g !== group.id);
551
+ }
552
+ updateGroupConfig('groups', groups);
452
553
  };
453
- row.appendChild(clearBtn);
454
- }
554
+ groupLabel.appendChild(groupCheck);
555
+ groupLabel.appendChild(document.createTextNode(group.name));
556
+ groupsList.appendChild(groupLabel);
557
+ });
558
+
559
+ groupsDiv.appendChild(groupsList);
560
+ content.appendChild(groupsDiv);
455
561
 
456
- content.appendChild(row);
562
+ // Action
563
+ const actionDiv = document.createElement('div');
564
+ actionDiv.style.cssText = 'margin-bottom:5px;';
565
+ const actionLabel = document.createElement('label');
566
+ actionLabel.style.cssText = 'font-size:11px;font-weight:600;display:block;margin-bottom:3px;color:#555;';
567
+ actionLabel.textContent = 'If user not in group:';
568
+ const actionSelect = document.createElement('select');
569
+ actionSelect.style.cssText = 'width:100%;padding:6px;font-size:12px;border:1px solid #ccc;border-radius:3px;';
570
+ actionSelect.innerHTML = '<option value="hide">hide</option><option value="disable">disable</option>';
571
+ actionSelect.value = dataConfig.action || 'hide';
572
+ actionSelect.onchange = () => updateGroupConfig('action', actionSelect.value);
573
+ actionDiv.appendChild(actionLabel);
574
+ actionDiv.appendChild(actionSelect);
575
+ content.appendChild(actionDiv);
457
576
  }
458
577
  /* Move to a Dock Spawn Helper */
459
578
  activateDockById(name) {
@@ -1,271 +1,65 @@
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;
128
-
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
- }
10
+ const result = await iobrokerHandler.checkVisibility(visibilityConfig);
147
11
 
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
- }
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);
182
- }
183
- }
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
27
  }
207
- }
208
- catch (err) {
209
- console.error('[Visibility] Revalidation error:', err);
28
+ } catch (err) {
29
+ console.error('[Visibility] Check failed:', err);
210
30
  }
211
31
  }
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
- */
226
- removeVisibility(element) {
227
- const intervalId = this.#intervalChecks.get(element);
228
- if (intervalId) {
229
- clearInterval(intervalId);
230
- this.#intervalChecks.delete(element);
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 || '');
231
38
  }
232
-
233
- this.#configs.delete(element);
234
- element.removeAttribute('data-visibility-controlled');
235
- element.removeAttribute('data-visibility-hash');
236
-
237
- // Restore original style
238
- const originalDisplay = this.#originalDisplayStyles.get(element);
239
- if (originalDisplay !== undefined) {
240
- element.style.display = originalDisplay;
39
+
40
+ await this.#checkAndApply(element, visibilityConfig);
41
+
42
+ if (visibilityConfig.objectId) {
43
+ await iobrokerHandler.subscribeState(visibilityConfig.objectId, () => {
44
+ this.#checkAndApply(element, visibilityConfig);
45
+ });
241
46
  }
242
- element.style.visibility = '';
243
- element.style.pointerEvents = '';
244
- element.style.opacity = '';
245
47
  }
246
-
247
- /**
248
- * Scan and apply visibility to all elements with visibility config
249
- */
48
+
250
49
  async scanAndApply(container = document.body) {
251
50
  const elements = container.querySelectorAll('[data-visibility-enabled="true"]');
252
- console.log(`🔍 [Visibility] Scanning ${elements.length} elements with visibility config`);
253
-
254
51
  for (const element of elements) {
255
52
  try {
256
- // Read config from separate attributes
257
53
  const visibilityConfig = {
258
- enabled: element.getAttribute('data-visibility-enabled') === 'true',
54
+ enabled: true,
259
55
  objectId: element.getAttribute('data-visibility-signal'),
260
56
  condition: element.getAttribute('data-visibility-condition') || '==',
261
57
  conditionValue: element.getAttribute('data-visibility-value') || '',
262
58
  action: element.getAttribute('data-visibility-action') || 'hide',
263
59
  groups: element.getAttribute('data-visibility-groups')?.split(',').filter(g => g) || []
264
60
  };
265
-
266
61
  await this.applyVisibility(element, visibilityConfig);
267
- }
268
- catch (err) {
62
+ } catch (err) {
269
63
  console.error('[Visibility] Error parsing visibility config:', err);
270
64
  }
271
65
  }