iobroker.mywebui 1.37.34 → 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.34",
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.34",
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",
@@ -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
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);
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,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
  }