myio-js-library 0.1.213 → 0.1.215

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.
@@ -21113,26 +21113,26 @@
21113
21113
  */
21114
21114
  getI18n() {
21115
21115
  const defaults = {
21116
- title: "Water Tank",
21117
- loading: "Loading...",
21118
- error: "Error loading data",
21119
- noData: "No data available",
21120
- exportCsv: "Export CSV",
21121
- close: "Close",
21122
- currentLevel: "Current Level",
21123
- averageLevel: "Average Level",
21124
- minLevel: "Minimum Level",
21125
- maxLevel: "Maximum Level",
21126
- dateRange: "Date Range",
21127
- deviceInfo: "Device Information",
21128
- levelChart: "Water Level History (m.c.a)",
21116
+ title: "Caixa d'\xC1gua",
21117
+ loading: "Carregando...",
21118
+ error: "Erro ao carregar dados",
21119
+ noData: "Nenhum dado dispon\xEDvel",
21120
+ exportCsv: "Exportar CSV",
21121
+ close: "Fechar",
21122
+ currentLevel: "N\xEDvel Atual",
21123
+ averageLevel: "N\xEDvel M\xE9dio",
21124
+ minLevel: "N\xEDvel M\xEDnimo",
21125
+ maxLevel: "N\xEDvel M\xE1ximo",
21126
+ dateRange: "Per\xEDodo",
21127
+ deviceInfo: "Informa\xE7\xF5es do Dispositivo",
21128
+ levelChart: "Hist\xF3rico de N\xEDvel (m.c.a)",
21129
21129
  percentUnit: "%",
21130
21130
  status: {
21131
- critical: "Critical",
21132
- low: "Low",
21133
- medium: "Medium",
21134
- good: "Good",
21135
- full: "Full"
21131
+ critical: "Cr\xEDtico",
21132
+ low: "Baixo",
21133
+ medium: "M\xE9dio",
21134
+ good: "Bom",
21135
+ full: "Cheio"
21136
21136
  }
21137
21137
  };
21138
21138
  return {
@@ -21233,62 +21233,327 @@
21233
21233
  this.attachEventListeners();
21234
21234
  }
21235
21235
  /**
21236
- * Render modal header
21236
+ * Render modal header - MyIO Premium Style
21237
21237
  */
21238
21238
  renderHeader() {
21239
21239
  const { context, params } = this.config;
21240
21240
  const title = params.ui?.title || `${this.i18n.title} - ${context.device.label}`;
21241
21241
  return `
21242
21242
  <div class="myio-water-tank-modal-header" style="
21243
- padding: 20px 24px;
21244
- border-bottom: 1px solid #e0e0e0;
21243
+ padding: 4px 8px;
21245
21244
  display: flex;
21246
21245
  align-items: center;
21247
21246
  justify-content: space-between;
21247
+ background: #3e1a7d;
21248
+ color: white;
21249
+ border-radius: 12px 12px 0 0;
21250
+ min-height: 20px;
21248
21251
  ">
21249
21252
  <h2 style="
21250
- margin: 0;
21251
- font-size: 20px;
21253
+ margin: 6px;
21254
+ font-size: 18px;
21252
21255
  font-weight: 600;
21253
- color: #2c3e50;
21254
- ">${title}</h2>
21255
- <button class="myio-water-tank-modal-close" style="
21256
- background: none;
21257
- border: none;
21258
- font-size: 24px;
21259
- color: #7f8c8d;
21260
- cursor: pointer;
21261
- padding: 0;
21262
- width: 32px;
21263
- height: 32px;
21264
- display: flex;
21265
- align-items: center;
21266
- justify-content: center;
21267
- border-radius: 4px;
21268
- transition: background 0.2s ease;
21269
- " title="${this.i18n.close}">
21270
- \xD7
21271
- </button>
21256
+ color: white;
21257
+ line-height: 2;
21258
+ ">\u{1F4A7} ${title}</h2>
21259
+ <div style="display: flex; gap: 4px; align-items: center;">
21260
+ <button class="myio-water-tank-modal-close" title="${this.i18n.close}" style="
21261
+ background: none;
21262
+ border: none;
21263
+ font-size: 20px;
21264
+ cursor: pointer;
21265
+ padding: 4px 8px;
21266
+ border-radius: 6px;
21267
+ color: rgba(255,255,255,0.8);
21268
+ transition: background-color 0.2s;
21269
+ ">\xD7</button>
21270
+ </div>
21272
21271
  </div>
21273
21272
  `;
21274
21273
  }
21275
21274
  /**
21276
- * Render modal body
21275
+ * RFC-0107: Render modal body with new layout
21276
+ * Left side: Tank visualization with percentage
21277
+ * Right side: Chart with controls (larger area)
21277
21278
  */
21278
21279
  renderBody() {
21279
21280
  return `
21280
21281
  <div class="myio-water-tank-modal-body" style="
21281
- padding: 24px;
21282
+ padding: 20px;
21282
21283
  overflow-y: auto;
21283
21284
  flex: 1;
21285
+ display: flex;
21286
+ flex-direction: column;
21287
+ gap: 16px;
21284
21288
  ">
21285
- ${this.renderDateRangePicker()}
21286
- ${this.renderTankVisualization()}
21287
- ${this.renderChart()}
21289
+ ${this.renderControlsBar()}
21290
+ <div style="
21291
+ display: flex;
21292
+ gap: 20px;
21293
+ flex: 1;
21294
+ min-height: 400px;
21295
+ ">
21296
+ ${this.renderTankPanel()}
21297
+ ${this.renderChartPanel()}
21298
+ </div>
21288
21299
  </div>
21289
21300
  ${this.renderFooter()}
21290
21301
  `;
21291
21302
  }
21303
+ /**
21304
+ * RFC-0107: Render controls bar with date range, aggregation, and limit
21305
+ */
21306
+ renderControlsBar() {
21307
+ const { params } = this.config;
21308
+ const startDate = this.formatDateForInput(params.startTs);
21309
+ const endDate = this.formatDateForInput(params.endTs);
21310
+ const currentAggregation = params.aggregation || "NONE";
21311
+ const currentLimit = params.limit || 1e3;
21312
+ return `
21313
+ <div style="
21314
+ background: #f8f9fa;
21315
+ border: 1px solid #e0e0e0;
21316
+ border-radius: 8px;
21317
+ padding: 12px 16px;
21318
+ display: flex;
21319
+ align-items: center;
21320
+ gap: 16px;
21321
+ flex-wrap: wrap;
21322
+ ">
21323
+ <div style="display: flex; align-items: center; gap: 8px;">
21324
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">De:</label>
21325
+ <input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
21326
+ padding: 6px 10px;
21327
+ border: 1px solid #ddd;
21328
+ border-radius: 6px;
21329
+ font-size: 13px;
21330
+ color: #2c3e50;
21331
+ cursor: pointer;
21332
+ "/>
21333
+ </div>
21334
+ <div style="display: flex; align-items: center; gap: 8px;">
21335
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">At\xE9:</label>
21336
+ <input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
21337
+ padding: 6px 10px;
21338
+ border: 1px solid #ddd;
21339
+ border-radius: 6px;
21340
+ font-size: 13px;
21341
+ color: #2c3e50;
21342
+ cursor: pointer;
21343
+ "/>
21344
+ </div>
21345
+ <div style="display: flex; align-items: center; gap: 8px;">
21346
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">Agrega\xE7\xE3o:</label>
21347
+ <select id="myio-water-tank-aggregation" style="
21348
+ padding: 6px 10px;
21349
+ border: 1px solid #ddd;
21350
+ border-radius: 6px;
21351
+ font-size: 13px;
21352
+ color: #2c3e50;
21353
+ background: white;
21354
+ cursor: pointer;
21355
+ ">
21356
+ <option value="NONE" ${currentAggregation === "NONE" ? "selected" : ""}>Nenhuma</option>
21357
+ <option value="AVG" ${currentAggregation === "AVG" ? "selected" : ""}>M\xE9dia</option>
21358
+ <option value="MIN" ${currentAggregation === "MIN" ? "selected" : ""}>M\xEDnimo</option>
21359
+ <option value="MAX" ${currentAggregation === "MAX" ? "selected" : ""}>M\xE1ximo</option>
21360
+ <option value="SUM" ${currentAggregation === "SUM" ? "selected" : ""}>Soma</option>
21361
+ <option value="COUNT" ${currentAggregation === "COUNT" ? "selected" : ""}>Contagem</option>
21362
+ </select>
21363
+ </div>
21364
+ <div style="display: flex; align-items: center; gap: 8px;">
21365
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">Limite:</label>
21366
+ <select id="myio-water-tank-limit" style="
21367
+ padding: 6px 10px;
21368
+ border: 1px solid #ddd;
21369
+ border-radius: 6px;
21370
+ font-size: 13px;
21371
+ color: #2c3e50;
21372
+ background: white;
21373
+ cursor: pointer;
21374
+ ">
21375
+ <option value="100" ${currentLimit === 100 ? "selected" : ""}>100</option>
21376
+ <option value="500" ${currentLimit === 500 ? "selected" : ""}>500</option>
21377
+ <option value="1000" ${currentLimit === 1e3 ? "selected" : ""}>1000</option>
21378
+ <option value="2000" ${currentLimit === 2e3 ? "selected" : ""}>2000</option>
21379
+ <option value="5000" ${currentLimit === 5e3 ? "selected" : ""}>5000</option>
21380
+ </select>
21381
+ </div>
21382
+ <button id="myio-water-tank-apply-dates" style="
21383
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
21384
+ color: white;
21385
+ border: none;
21386
+ padding: 6px 16px;
21387
+ border-radius: 6px;
21388
+ font-size: 13px;
21389
+ font-weight: 500;
21390
+ cursor: pointer;
21391
+ transition: all 0.2s ease;
21392
+ ">
21393
+ Aplicar
21394
+ </button>
21395
+ </div>
21396
+ `;
21397
+ }
21398
+ /**
21399
+ * RFC-0107: Render tank panel (left side)
21400
+ */
21401
+ renderTankPanel() {
21402
+ const { data, context } = this.config;
21403
+ let percentage = 0;
21404
+ const percentagePoints = data.telemetry.filter((p) => p.key === "water_percentage");
21405
+ if (percentagePoints.length > 0) {
21406
+ const latestPercentage = percentagePoints[percentagePoints.length - 1].value;
21407
+ percentage = latestPercentage <= 1.5 ? latestPercentage * 100 : latestPercentage;
21408
+ } else if (context.device.currentLevel !== void 0) {
21409
+ const level = context.device.currentLevel;
21410
+ percentage = level <= 1.5 ? level * 100 : level;
21411
+ }
21412
+ const levelStatus = this.getLevelStatus(Math.min(percentage, 100));
21413
+ const tankImageUrl = this.getTankImageUrl(Math.min(percentage, 100));
21414
+ const displayPercentage = percentage.toFixed(1);
21415
+ return `
21416
+ <div style="
21417
+ width: 200px;
21418
+ min-width: 200px;
21419
+ background: linear-gradient(135deg, ${levelStatus.color}10 0%, ${levelStatus.color}05 100%);
21420
+ border: 1px solid ${levelStatus.color}30;
21421
+ border-radius: 12px;
21422
+ padding: 24px 16px;
21423
+ display: flex;
21424
+ flex-direction: column;
21425
+ align-items: center;
21426
+ justify-content: center;
21427
+ gap: 16px;
21428
+ ">
21429
+ <img src="${tankImageUrl}" alt="Water Tank" style="
21430
+ width: 100px;
21431
+ height: auto;
21432
+ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
21433
+ "/>
21434
+ <div style="
21435
+ font-size: 42px;
21436
+ font-weight: 700;
21437
+ color: ${levelStatus.color};
21438
+ line-height: 1;
21439
+ ">${displayPercentage}%</div>
21440
+ <div style="
21441
+ background: ${levelStatus.color};
21442
+ color: white;
21443
+ padding: 4px 12px;
21444
+ border-radius: 20px;
21445
+ font-size: 11px;
21446
+ font-weight: 600;
21447
+ text-transform: uppercase;
21448
+ ">${levelStatus.label}</div>
21449
+ <div style="
21450
+ font-size: 12px;
21451
+ color: #7f8c8d;
21452
+ text-align: center;
21453
+ ">${this.i18n.currentLevel}</div>
21454
+ </div>
21455
+ `;
21456
+ }
21457
+ /**
21458
+ * RFC-0107: Render chart panel (right side) with maximize button
21459
+ */
21460
+ renderChartPanel() {
21461
+ const chartPoints = this.getChartDataPoints();
21462
+ const chartTitle = this.chartDisplayMode === "water_percentage" ? "Hist\xF3rico de N\xEDvel (%)" : this.i18n.levelChart;
21463
+ if (chartPoints.length === 0) {
21464
+ const displayLabel = this.chartDisplayMode === "water_percentage" ? "%" : "m.c.a";
21465
+ return `
21466
+ <div style="
21467
+ flex: 1;
21468
+ background: #f8f9fa;
21469
+ border: 1px solid #e0e0e0;
21470
+ border-radius: 12px;
21471
+ display: flex;
21472
+ flex-direction: column;
21473
+ align-items: center;
21474
+ justify-content: center;
21475
+ padding: 24px;
21476
+ ">
21477
+ <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;">\u{1F4CA}</div>
21478
+ <div style="color: #7f8c8d; font-size: 16px;">${this.i18n.noData}</div>
21479
+ <div style="color: #bdc3c7; font-size: 13px; margin-top: 8px;">
21480
+ Sem dados de ${this.chartDisplayMode === "water_percentage" ? "percentual" : "n\xEDvel"} (${displayLabel}) dispon\xEDveis
21481
+ </div>
21482
+ </div>
21483
+ `;
21484
+ }
21485
+ const firstTs = chartPoints[0]?.ts;
21486
+ const lastTs = chartPoints[chartPoints.length - 1]?.ts;
21487
+ return `
21488
+ <div id="myio-water-tank-chart-panel" style="
21489
+ flex: 1;
21490
+ background: white;
21491
+ border: 1px solid #e0e0e0;
21492
+ border-radius: 12px;
21493
+ padding: 16px;
21494
+ display: flex;
21495
+ flex-direction: column;
21496
+ position: relative;
21497
+ ">
21498
+ <div style="
21499
+ display: flex;
21500
+ align-items: center;
21501
+ justify-content: space-between;
21502
+ margin-bottom: 12px;
21503
+ ">
21504
+ <h3 style="
21505
+ margin: 0;
21506
+ font-size: 15px;
21507
+ font-weight: 600;
21508
+ color: #2c3e50;
21509
+ ">${chartTitle}</h3>
21510
+ <div style="
21511
+ display: flex;
21512
+ align-items: center;
21513
+ gap: 8px;
21514
+ ">
21515
+ <select id="myio-water-tank-display-mode" style="
21516
+ padding: 4px 8px;
21517
+ border: 1px solid #ddd;
21518
+ border-radius: 4px;
21519
+ font-size: 12px;
21520
+ color: #2c3e50;
21521
+ background: white;
21522
+ cursor: pointer;
21523
+ ">
21524
+ <option value="water_level" ${this.chartDisplayMode === "water_level" ? "selected" : ""}>N\xEDvel (m.c.a)</option>
21525
+ <option value="water_percentage" ${this.chartDisplayMode === "water_percentage" ? "selected" : ""}>Percentual (%)</option>
21526
+ </select>
21527
+ <button id="myio-water-tank-maximize" title="Maximizar gr\xE1fico" style="
21528
+ background: #f0f0f0;
21529
+ border: 1px solid #ddd;
21530
+ border-radius: 4px;
21531
+ padding: 4px 8px;
21532
+ cursor: pointer;
21533
+ font-size: 14px;
21534
+ display: flex;
21535
+ align-items: center;
21536
+ justify-content: center;
21537
+ ">\u26F6</button>
21538
+ </div>
21539
+ </div>
21540
+ <div style="flex: 1; min-height: 300px;">
21541
+ <canvas id="myio-water-tank-chart" style="width: 100%; height: 100%;"></canvas>
21542
+ </div>
21543
+ ${firstTs && lastTs ? `
21544
+ <div style="
21545
+ margin-top: 8px;
21546
+ font-size: 11px;
21547
+ color: #7f8c8d;
21548
+ text-align: center;
21549
+ ">
21550
+ ${this.formatDate(firstTs, false)} \u2014 ${this.formatDate(lastTs, false)}
21551
+ (${chartPoints.length} leituras)
21552
+ </div>
21553
+ ` : ""}
21554
+ </div>
21555
+ `;
21556
+ }
21292
21557
  /**
21293
21558
  * Render date range picker
21294
21559
  */
@@ -21309,7 +21574,7 @@
21309
21574
  flex-wrap: wrap;
21310
21575
  ">
21311
21576
  <div style="display: flex; align-items: center; gap: 8px;">
21312
- <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">From:</label>
21577
+ <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">De:</label>
21313
21578
  <input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
21314
21579
  padding: 8px 12px;
21315
21580
  border: 1px solid #ddd;
@@ -21320,7 +21585,7 @@
21320
21585
  "/>
21321
21586
  </div>
21322
21587
  <div style="display: flex; align-items: center; gap: 8px;">
21323
- <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">To:</label>
21588
+ <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">At\xE9:</label>
21324
21589
  <input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
21325
21590
  padding: 8px 12px;
21326
21591
  border: 1px solid #ddd;
@@ -21341,7 +21606,7 @@
21341
21606
  cursor: pointer;
21342
21607
  transition: all 0.2s ease;
21343
21608
  ">
21344
- Apply
21609
+ Aplicar
21345
21610
  </button>
21346
21611
  </div>
21347
21612
  `;
@@ -21563,7 +21828,7 @@
21563
21828
  }
21564
21829
  const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
21565
21830
  if (applyDatesBtn) {
21566
- applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
21831
+ applyDatesBtn.addEventListener("click", () => this.handleApplyParams());
21567
21832
  }
21568
21833
  const displayModeSelect = this.modal.querySelector("#myio-water-tank-display-mode");
21569
21834
  if (displayModeSelect) {
@@ -21572,6 +21837,10 @@
21572
21837
  this.refreshChart();
21573
21838
  });
21574
21839
  }
21840
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
21841
+ if (maximizeBtn) {
21842
+ maximizeBtn.addEventListener("click", () => this.handleMaximize());
21843
+ }
21575
21844
  this.overlay.addEventListener("click", (e) => {
21576
21845
  if (e.target === this.overlay) {
21577
21846
  this.config.onClose();
@@ -21586,12 +21855,20 @@
21586
21855
  });
21587
21856
  }
21588
21857
  /**
21589
- * Handle date range change
21858
+ * Handle date range change (legacy - kept for compatibility)
21590
21859
  */
21591
21860
  handleDateRangeChange() {
21861
+ this.handleApplyParams();
21862
+ }
21863
+ /**
21864
+ * RFC-0107: Handle apply params (date range, aggregation, limit)
21865
+ */
21866
+ handleApplyParams() {
21592
21867
  if (!this.modal) return;
21593
21868
  const startInput = this.modal.querySelector("#myio-water-tank-start-date");
21594
21869
  const endInput = this.modal.querySelector("#myio-water-tank-end-date");
21870
+ const aggregationSelect = this.modal.querySelector("#myio-water-tank-aggregation");
21871
+ const limitSelect = this.modal.querySelector("#myio-water-tank-limit");
21595
21872
  if (startInput && endInput) {
21596
21873
  const startTs = new Date(startInput.value).setHours(0, 0, 0, 0);
21597
21874
  const endTs = new Date(endInput.value).setHours(23, 59, 59, 999);
@@ -21599,17 +21876,77 @@
21599
21876
  alert("Start date must be before end date");
21600
21877
  return;
21601
21878
  }
21602
- console.log("[WaterTankModalView] Date range changed:", {
21879
+ const aggregation = aggregationSelect?.value || "NONE";
21880
+ const limit = parseInt(limitSelect?.value || "1000", 10);
21881
+ console.log("[WaterTankModalView] Params changed:", {
21603
21882
  startTs,
21604
21883
  endTs,
21884
+ aggregation,
21885
+ limit,
21605
21886
  startDate: new Date(startTs).toISOString(),
21606
21887
  endDate: new Date(endTs).toISOString()
21607
21888
  });
21608
- if (this.config.onDateRangeChange) {
21889
+ this.config.params.startTs = startTs;
21890
+ this.config.params.endTs = endTs;
21891
+ this.config.params.aggregation = aggregation;
21892
+ this.config.params.limit = limit;
21893
+ if (this.config.onParamsChange) {
21894
+ this.config.onParamsChange({ startTs, endTs, aggregation, limit });
21895
+ } else if (this.config.onDateRangeChange) {
21609
21896
  this.config.onDateRangeChange(startTs, endTs);
21610
21897
  }
21611
21898
  }
21612
21899
  }
21900
+ /**
21901
+ * RFC-0107: Handle maximize/restore chart
21902
+ */
21903
+ isMaximized = false;
21904
+ originalModalStyle = "";
21905
+ handleMaximize() {
21906
+ if (!this.modal) return;
21907
+ const chartPanel = this.modal.querySelector("#myio-water-tank-chart-panel");
21908
+ const tankPanel = chartPanel?.previousElementSibling;
21909
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
21910
+ if (!chartPanel) return;
21911
+ if (this.isMaximized) {
21912
+ this.modal.style.cssText = this.originalModalStyle;
21913
+ if (tankPanel) tankPanel.style.display = "";
21914
+ chartPanel.style.cssText = `
21915
+ flex: 1;
21916
+ background: white;
21917
+ border: 1px solid #e0e0e0;
21918
+ border-radius: 12px;
21919
+ padding: 16px;
21920
+ display: flex;
21921
+ flex-direction: column;
21922
+ position: relative;
21923
+ `;
21924
+ if (maximizeBtn) maximizeBtn.textContent = "\u26F6";
21925
+ this.isMaximized = false;
21926
+ } else {
21927
+ this.originalModalStyle = this.modal.style.cssText;
21928
+ this.modal.style.width = "95vw";
21929
+ this.modal.style.height = "90vh";
21930
+ this.modal.style.maxWidth = "95vw";
21931
+ if (tankPanel) tankPanel.style.display = "none";
21932
+ chartPanel.style.cssText = `
21933
+ flex: 1;
21934
+ background: white;
21935
+ border: 1px solid #e0e0e0;
21936
+ border-radius: 12px;
21937
+ padding: 16px;
21938
+ display: flex;
21939
+ flex-direction: column;
21940
+ position: relative;
21941
+ min-height: 100%;
21942
+ `;
21943
+ if (maximizeBtn) maximizeBtn.textContent = "\u26F6";
21944
+ this.isMaximized = true;
21945
+ }
21946
+ requestAnimationFrame(() => {
21947
+ this.renderCanvasChart();
21948
+ });
21949
+ }
21613
21950
  handleEscapeKey(e) {
21614
21951
  if (e.key === "Escape") {
21615
21952
  this.config.onClose();
@@ -21748,7 +22085,7 @@
21748
22085
  }
21749
22086
  }
21750
22087
  /**
21751
- * Update data and re-render chart
22088
+ * Update data and re-render chart with new layout
21752
22089
  */
21753
22090
  updateData(data) {
21754
22091
  this.config.data = data;
@@ -21756,13 +22093,20 @@
21756
22093
  const bodyEl = this.modal.querySelector(".myio-water-tank-modal-body");
21757
22094
  if (bodyEl) {
21758
22095
  bodyEl.innerHTML = `
21759
- ${this.renderDateRangePicker()}
21760
- ${this.renderTankVisualization()}
21761
- ${this.renderChart()}
22096
+ ${this.renderControlsBar()}
22097
+ <div style="
22098
+ display: flex;
22099
+ gap: 20px;
22100
+ flex: 1;
22101
+ min-height: 400px;
22102
+ ">
22103
+ ${this.renderTankPanel()}
22104
+ ${this.renderChartPanel()}
22105
+ </div>
21762
22106
  `;
21763
22107
  const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
21764
22108
  if (applyDatesBtn) {
21765
- applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
22109
+ applyDatesBtn.addEventListener("click", () => this.handleApplyParams());
21766
22110
  }
21767
22111
  const displayModeSelect = this.modal.querySelector("#myio-water-tank-display-mode");
21768
22112
  if (displayModeSelect) {
@@ -21771,6 +22115,10 @@
21771
22115
  this.refreshChart();
21772
22116
  });
21773
22117
  }
22118
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
22119
+ if (maximizeBtn) {
22120
+ maximizeBtn.addEventListener("click", () => this.handleMaximize());
22121
+ }
21774
22122
  requestAnimationFrame(() => {
21775
22123
  this.renderCanvasChart();
21776
22124
  });
@@ -22050,7 +22398,8 @@
22050
22398
  onError: (error) => this.handleError(error),
22051
22399
  onClose: () => this.close(),
22052
22400
  // Call close() to destroy view and trigger user callback
22053
- onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs)
22401
+ onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs),
22402
+ onParamsChange: (params) => this.handleParamsChange(params)
22054
22403
  });
22055
22404
  this.view.render();
22056
22405
  this.view.show();
@@ -22115,6 +22464,43 @@
22115
22464
  this.handleError(error);
22116
22465
  }
22117
22466
  }
22467
+ /**
22468
+ * RFC-0107: Handle params change (date range, aggregation, limit)
22469
+ */
22470
+ async handleParamsChange(params) {
22471
+ console.log("[WaterTankModal] Params changed:", {
22472
+ startTs: params.startTs,
22473
+ endTs: params.endTs,
22474
+ aggregation: params.aggregation,
22475
+ limit: params.limit,
22476
+ startDate: new Date(params.startTs).toISOString(),
22477
+ endDate: new Date(params.endTs).toISOString()
22478
+ });
22479
+ this.options.startTs = params.startTs;
22480
+ this.options.endTs = params.endTs;
22481
+ this.options.aggregation = params.aggregation;
22482
+ this.options.limit = params.limit;
22483
+ this.context.timeRange.startTs = params.startTs;
22484
+ this.context.timeRange.endTs = params.endTs;
22485
+ try {
22486
+ console.log("[WaterTankModal] Fetching data with new params...");
22487
+ this.data = await this.fetchTelemetryData();
22488
+ if (this.view) {
22489
+ this.view.updateData(this.data);
22490
+ }
22491
+ if (this.options.onDataLoaded) {
22492
+ try {
22493
+ this.options.onDataLoaded(this.data);
22494
+ } catch (callbackError) {
22495
+ console.warn("[WaterTankModal] onDataLoaded callback error:", callbackError);
22496
+ }
22497
+ }
22498
+ console.log("[WaterTankModal] Data refreshed with new params successfully");
22499
+ } catch (error) {
22500
+ console.error("[WaterTankModal] Failed to fetch data with new params:", error);
22501
+ this.handleError(error);
22502
+ }
22503
+ }
22118
22504
  /**
22119
22505
  * Handle export functionality
22120
22506
  */
@@ -22236,8 +22622,8 @@
22236
22622
  }
22237
22623
  if (options.currentLevel !== void 0) {
22238
22624
  const level = Number(options.currentLevel);
22239
- if (isNaN(level) || level < 0 || level > 100) {
22240
- errors.push("currentLevel must be a number between 0 and 100");
22625
+ if (isNaN(level) || level < 0) {
22626
+ errors.push("currentLevel must be a non-negative number");
22241
22627
  }
22242
22628
  }
22243
22629
  if (options.limit !== void 0) {
@@ -28462,13 +28848,31 @@
28462
28848
  };
28463
28849
 
28464
28850
  // src/components/premium-modals/settings/SettingsFetcher.ts
28465
- var DefaultSettingsFetcher = class {
28851
+ var DefaultSettingsFetcher = class _DefaultSettingsFetcher {
28466
28852
  jwtToken;
28467
28853
  tbBaseUrl;
28854
+ static FETCH_TIMEOUT_MS = 8e3;
28855
+ // 8 second timeout
28468
28856
  constructor(jwtToken, apiConfig) {
28469
28857
  this.jwtToken = jwtToken;
28470
28858
  this.tbBaseUrl = apiConfig?.tbBaseUrl || window.location.origin;
28471
28859
  }
28860
+ /**
28861
+ * Fetch with timeout to prevent hanging requests from blocking modal render
28862
+ */
28863
+ async fetchWithTimeout(url, options, timeoutMs = _DefaultSettingsFetcher.FETCH_TIMEOUT_MS) {
28864
+ const controller = new AbortController();
28865
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
28866
+ try {
28867
+ const response = await fetch(url, {
28868
+ ...options,
28869
+ signal: controller.signal
28870
+ });
28871
+ return response;
28872
+ } finally {
28873
+ clearTimeout(timeoutId);
28874
+ }
28875
+ }
28472
28876
  async fetchCurrentSettings(deviceId, jwtToken, scope = "SERVER_SCOPE") {
28473
28877
  try {
28474
28878
  const [entityResult, attributesResult] = await Promise.allSettled([
@@ -28502,9 +28906,12 @@
28502
28906
  }
28503
28907
  }
28504
28908
  async fetchDeviceEntity(deviceId) {
28505
- const response = await fetch(`${this.tbBaseUrl}/api/device/${deviceId}`, {
28506
- headers: { "X-Authorization": `Bearer ${this.jwtToken}` }
28507
- });
28909
+ const response = await this.fetchWithTimeout(
28910
+ `${this.tbBaseUrl}/api/device/${deviceId}`,
28911
+ {
28912
+ headers: { "X-Authorization": `Bearer ${this.jwtToken}` }
28913
+ }
28914
+ );
28508
28915
  if (!response.ok) {
28509
28916
  throw new Error(
28510
28917
  `Failed to fetch device entity: ${response.status} ${response.statusText}`
@@ -28516,7 +28923,7 @@
28516
28923
  };
28517
28924
  }
28518
28925
  async fetchDeviceAttributes(deviceId, scope) {
28519
- const response = await fetch(
28926
+ const response = await this.fetchWithTimeout(
28520
28927
  `${this.tbBaseUrl}/api/plugins/telemetry/DEVICE/${deviceId}/values/attributes/${scope}`,
28521
28928
  {
28522
28929
  headers: { "X-Authorization": `Bearer ${this.jwtToken}` }
@@ -28725,12 +29132,16 @@
28725
29132
  const tbBaseUrl = this.params.api?.tbBaseUrl || window.location.origin;
28726
29133
  const url = `${tbBaseUrl}/api/plugins/telemetry/CUSTOMER/${customerId}/values/attributes/SERVER_SCOPE?keys=mapInstantaneousPower`;
28727
29134
  console.log("[SettingsModal] RFC-0080: Fetching GLOBAL from:", url);
29135
+ const controller = new AbortController();
29136
+ const timeoutId = setTimeout(() => controller.abort(), 8e3);
28728
29137
  const response = await fetch(url, {
28729
29138
  headers: {
28730
29139
  "X-Authorization": `Bearer ${jwtToken}`,
28731
29140
  "Content-Type": "application/json"
28732
- }
29141
+ },
29142
+ signal: controller.signal
28733
29143
  });
29144
+ clearTimeout(timeoutId);
28734
29145
  if (!response.ok) {
28735
29146
  throw new Error(`HTTP ${response.status}`);
28736
29147
  }