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.
package/dist/index.js CHANGED
@@ -21299,26 +21299,26 @@ var WaterTankModalView = class {
21299
21299
  */
21300
21300
  getI18n() {
21301
21301
  const defaults = {
21302
- title: "Water Tank",
21303
- loading: "Loading...",
21304
- error: "Error loading data",
21305
- noData: "No data available",
21306
- exportCsv: "Export CSV",
21307
- close: "Close",
21308
- currentLevel: "Current Level",
21309
- averageLevel: "Average Level",
21310
- minLevel: "Minimum Level",
21311
- maxLevel: "Maximum Level",
21312
- dateRange: "Date Range",
21313
- deviceInfo: "Device Information",
21314
- levelChart: "Water Level History (m.c.a)",
21302
+ title: "Caixa d'\xC1gua",
21303
+ loading: "Carregando...",
21304
+ error: "Erro ao carregar dados",
21305
+ noData: "Nenhum dado dispon\xEDvel",
21306
+ exportCsv: "Exportar CSV",
21307
+ close: "Fechar",
21308
+ currentLevel: "N\xEDvel Atual",
21309
+ averageLevel: "N\xEDvel M\xE9dio",
21310
+ minLevel: "N\xEDvel M\xEDnimo",
21311
+ maxLevel: "N\xEDvel M\xE1ximo",
21312
+ dateRange: "Per\xEDodo",
21313
+ deviceInfo: "Informa\xE7\xF5es do Dispositivo",
21314
+ levelChart: "Hist\xF3rico de N\xEDvel (m.c.a)",
21315
21315
  percentUnit: "%",
21316
21316
  status: {
21317
- critical: "Critical",
21318
- low: "Low",
21319
- medium: "Medium",
21320
- good: "Good",
21321
- full: "Full"
21317
+ critical: "Cr\xEDtico",
21318
+ low: "Baixo",
21319
+ medium: "M\xE9dio",
21320
+ good: "Bom",
21321
+ full: "Cheio"
21322
21322
  }
21323
21323
  };
21324
21324
  return {
@@ -21419,62 +21419,327 @@ var WaterTankModalView = class {
21419
21419
  this.attachEventListeners();
21420
21420
  }
21421
21421
  /**
21422
- * Render modal header
21422
+ * Render modal header - MyIO Premium Style
21423
21423
  */
21424
21424
  renderHeader() {
21425
21425
  const { context, params } = this.config;
21426
21426
  const title = params.ui?.title || `${this.i18n.title} - ${context.device.label}`;
21427
21427
  return `
21428
21428
  <div class="myio-water-tank-modal-header" style="
21429
- padding: 20px 24px;
21430
- border-bottom: 1px solid #e0e0e0;
21429
+ padding: 4px 8px;
21431
21430
  display: flex;
21432
21431
  align-items: center;
21433
21432
  justify-content: space-between;
21433
+ background: #3e1a7d;
21434
+ color: white;
21435
+ border-radius: 12px 12px 0 0;
21436
+ min-height: 20px;
21434
21437
  ">
21435
21438
  <h2 style="
21436
- margin: 0;
21437
- font-size: 20px;
21439
+ margin: 6px;
21440
+ font-size: 18px;
21438
21441
  font-weight: 600;
21439
- color: #2c3e50;
21440
- ">${title}</h2>
21441
- <button class="myio-water-tank-modal-close" style="
21442
- background: none;
21443
- border: none;
21444
- font-size: 24px;
21445
- color: #7f8c8d;
21446
- cursor: pointer;
21447
- padding: 0;
21448
- width: 32px;
21449
- height: 32px;
21450
- display: flex;
21451
- align-items: center;
21452
- justify-content: center;
21453
- border-radius: 4px;
21454
- transition: background 0.2s ease;
21455
- " title="${this.i18n.close}">
21456
- \xD7
21457
- </button>
21442
+ color: white;
21443
+ line-height: 2;
21444
+ ">\u{1F4A7} ${title}</h2>
21445
+ <div style="display: flex; gap: 4px; align-items: center;">
21446
+ <button class="myio-water-tank-modal-close" title="${this.i18n.close}" style="
21447
+ background: none;
21448
+ border: none;
21449
+ font-size: 20px;
21450
+ cursor: pointer;
21451
+ padding: 4px 8px;
21452
+ border-radius: 6px;
21453
+ color: rgba(255,255,255,0.8);
21454
+ transition: background-color 0.2s;
21455
+ ">\xD7</button>
21456
+ </div>
21458
21457
  </div>
21459
21458
  `;
21460
21459
  }
21461
21460
  /**
21462
- * Render modal body
21461
+ * RFC-0107: Render modal body with new layout
21462
+ * Left side: Tank visualization with percentage
21463
+ * Right side: Chart with controls (larger area)
21463
21464
  */
21464
21465
  renderBody() {
21465
21466
  return `
21466
21467
  <div class="myio-water-tank-modal-body" style="
21467
- padding: 24px;
21468
+ padding: 20px;
21468
21469
  overflow-y: auto;
21469
21470
  flex: 1;
21471
+ display: flex;
21472
+ flex-direction: column;
21473
+ gap: 16px;
21470
21474
  ">
21471
- ${this.renderDateRangePicker()}
21472
- ${this.renderTankVisualization()}
21473
- ${this.renderChart()}
21475
+ ${this.renderControlsBar()}
21476
+ <div style="
21477
+ display: flex;
21478
+ gap: 20px;
21479
+ flex: 1;
21480
+ min-height: 400px;
21481
+ ">
21482
+ ${this.renderTankPanel()}
21483
+ ${this.renderChartPanel()}
21484
+ </div>
21474
21485
  </div>
21475
21486
  ${this.renderFooter()}
21476
21487
  `;
21477
21488
  }
21489
+ /**
21490
+ * RFC-0107: Render controls bar with date range, aggregation, and limit
21491
+ */
21492
+ renderControlsBar() {
21493
+ const { params } = this.config;
21494
+ const startDate = this.formatDateForInput(params.startTs);
21495
+ const endDate = this.formatDateForInput(params.endTs);
21496
+ const currentAggregation = params.aggregation || "NONE";
21497
+ const currentLimit = params.limit || 1e3;
21498
+ return `
21499
+ <div style="
21500
+ background: #f8f9fa;
21501
+ border: 1px solid #e0e0e0;
21502
+ border-radius: 8px;
21503
+ padding: 12px 16px;
21504
+ display: flex;
21505
+ align-items: center;
21506
+ gap: 16px;
21507
+ flex-wrap: wrap;
21508
+ ">
21509
+ <div style="display: flex; align-items: center; gap: 8px;">
21510
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">De:</label>
21511
+ <input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
21512
+ padding: 6px 10px;
21513
+ border: 1px solid #ddd;
21514
+ border-radius: 6px;
21515
+ font-size: 13px;
21516
+ color: #2c3e50;
21517
+ cursor: pointer;
21518
+ "/>
21519
+ </div>
21520
+ <div style="display: flex; align-items: center; gap: 8px;">
21521
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">At\xE9:</label>
21522
+ <input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
21523
+ padding: 6px 10px;
21524
+ border: 1px solid #ddd;
21525
+ border-radius: 6px;
21526
+ font-size: 13px;
21527
+ color: #2c3e50;
21528
+ cursor: pointer;
21529
+ "/>
21530
+ </div>
21531
+ <div style="display: flex; align-items: center; gap: 8px;">
21532
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">Agrega\xE7\xE3o:</label>
21533
+ <select id="myio-water-tank-aggregation" style="
21534
+ padding: 6px 10px;
21535
+ border: 1px solid #ddd;
21536
+ border-radius: 6px;
21537
+ font-size: 13px;
21538
+ color: #2c3e50;
21539
+ background: white;
21540
+ cursor: pointer;
21541
+ ">
21542
+ <option value="NONE" ${currentAggregation === "NONE" ? "selected" : ""}>Nenhuma</option>
21543
+ <option value="AVG" ${currentAggregation === "AVG" ? "selected" : ""}>M\xE9dia</option>
21544
+ <option value="MIN" ${currentAggregation === "MIN" ? "selected" : ""}>M\xEDnimo</option>
21545
+ <option value="MAX" ${currentAggregation === "MAX" ? "selected" : ""}>M\xE1ximo</option>
21546
+ <option value="SUM" ${currentAggregation === "SUM" ? "selected" : ""}>Soma</option>
21547
+ <option value="COUNT" ${currentAggregation === "COUNT" ? "selected" : ""}>Contagem</option>
21548
+ </select>
21549
+ </div>
21550
+ <div style="display: flex; align-items: center; gap: 8px;">
21551
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">Limite:</label>
21552
+ <select id="myio-water-tank-limit" style="
21553
+ padding: 6px 10px;
21554
+ border: 1px solid #ddd;
21555
+ border-radius: 6px;
21556
+ font-size: 13px;
21557
+ color: #2c3e50;
21558
+ background: white;
21559
+ cursor: pointer;
21560
+ ">
21561
+ <option value="100" ${currentLimit === 100 ? "selected" : ""}>100</option>
21562
+ <option value="500" ${currentLimit === 500 ? "selected" : ""}>500</option>
21563
+ <option value="1000" ${currentLimit === 1e3 ? "selected" : ""}>1000</option>
21564
+ <option value="2000" ${currentLimit === 2e3 ? "selected" : ""}>2000</option>
21565
+ <option value="5000" ${currentLimit === 5e3 ? "selected" : ""}>5000</option>
21566
+ </select>
21567
+ </div>
21568
+ <button id="myio-water-tank-apply-dates" style="
21569
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
21570
+ color: white;
21571
+ border: none;
21572
+ padding: 6px 16px;
21573
+ border-radius: 6px;
21574
+ font-size: 13px;
21575
+ font-weight: 500;
21576
+ cursor: pointer;
21577
+ transition: all 0.2s ease;
21578
+ ">
21579
+ Aplicar
21580
+ </button>
21581
+ </div>
21582
+ `;
21583
+ }
21584
+ /**
21585
+ * RFC-0107: Render tank panel (left side)
21586
+ */
21587
+ renderTankPanel() {
21588
+ const { data, context } = this.config;
21589
+ let percentage = 0;
21590
+ const percentagePoints = data.telemetry.filter((p) => p.key === "water_percentage");
21591
+ if (percentagePoints.length > 0) {
21592
+ const latestPercentage = percentagePoints[percentagePoints.length - 1].value;
21593
+ percentage = latestPercentage <= 1.5 ? latestPercentage * 100 : latestPercentage;
21594
+ } else if (context.device.currentLevel !== void 0) {
21595
+ const level = context.device.currentLevel;
21596
+ percentage = level <= 1.5 ? level * 100 : level;
21597
+ }
21598
+ const levelStatus = this.getLevelStatus(Math.min(percentage, 100));
21599
+ const tankImageUrl = this.getTankImageUrl(Math.min(percentage, 100));
21600
+ const displayPercentage = percentage.toFixed(1);
21601
+ return `
21602
+ <div style="
21603
+ width: 200px;
21604
+ min-width: 200px;
21605
+ background: linear-gradient(135deg, ${levelStatus.color}10 0%, ${levelStatus.color}05 100%);
21606
+ border: 1px solid ${levelStatus.color}30;
21607
+ border-radius: 12px;
21608
+ padding: 24px 16px;
21609
+ display: flex;
21610
+ flex-direction: column;
21611
+ align-items: center;
21612
+ justify-content: center;
21613
+ gap: 16px;
21614
+ ">
21615
+ <img src="${tankImageUrl}" alt="Water Tank" style="
21616
+ width: 100px;
21617
+ height: auto;
21618
+ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
21619
+ "/>
21620
+ <div style="
21621
+ font-size: 42px;
21622
+ font-weight: 700;
21623
+ color: ${levelStatus.color};
21624
+ line-height: 1;
21625
+ ">${displayPercentage}%</div>
21626
+ <div style="
21627
+ background: ${levelStatus.color};
21628
+ color: white;
21629
+ padding: 4px 12px;
21630
+ border-radius: 20px;
21631
+ font-size: 11px;
21632
+ font-weight: 600;
21633
+ text-transform: uppercase;
21634
+ ">${levelStatus.label}</div>
21635
+ <div style="
21636
+ font-size: 12px;
21637
+ color: #7f8c8d;
21638
+ text-align: center;
21639
+ ">${this.i18n.currentLevel}</div>
21640
+ </div>
21641
+ `;
21642
+ }
21643
+ /**
21644
+ * RFC-0107: Render chart panel (right side) with maximize button
21645
+ */
21646
+ renderChartPanel() {
21647
+ const chartPoints = this.getChartDataPoints();
21648
+ const chartTitle = this.chartDisplayMode === "water_percentage" ? "Hist\xF3rico de N\xEDvel (%)" : this.i18n.levelChart;
21649
+ if (chartPoints.length === 0) {
21650
+ const displayLabel = this.chartDisplayMode === "water_percentage" ? "%" : "m.c.a";
21651
+ return `
21652
+ <div style="
21653
+ flex: 1;
21654
+ background: #f8f9fa;
21655
+ border: 1px solid #e0e0e0;
21656
+ border-radius: 12px;
21657
+ display: flex;
21658
+ flex-direction: column;
21659
+ align-items: center;
21660
+ justify-content: center;
21661
+ padding: 24px;
21662
+ ">
21663
+ <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;">\u{1F4CA}</div>
21664
+ <div style="color: #7f8c8d; font-size: 16px;">${this.i18n.noData}</div>
21665
+ <div style="color: #bdc3c7; font-size: 13px; margin-top: 8px;">
21666
+ Sem dados de ${this.chartDisplayMode === "water_percentage" ? "percentual" : "n\xEDvel"} (${displayLabel}) dispon\xEDveis
21667
+ </div>
21668
+ </div>
21669
+ `;
21670
+ }
21671
+ const firstTs = chartPoints[0]?.ts;
21672
+ const lastTs = chartPoints[chartPoints.length - 1]?.ts;
21673
+ return `
21674
+ <div id="myio-water-tank-chart-panel" style="
21675
+ flex: 1;
21676
+ background: white;
21677
+ border: 1px solid #e0e0e0;
21678
+ border-radius: 12px;
21679
+ padding: 16px;
21680
+ display: flex;
21681
+ flex-direction: column;
21682
+ position: relative;
21683
+ ">
21684
+ <div style="
21685
+ display: flex;
21686
+ align-items: center;
21687
+ justify-content: space-between;
21688
+ margin-bottom: 12px;
21689
+ ">
21690
+ <h3 style="
21691
+ margin: 0;
21692
+ font-size: 15px;
21693
+ font-weight: 600;
21694
+ color: #2c3e50;
21695
+ ">${chartTitle}</h3>
21696
+ <div style="
21697
+ display: flex;
21698
+ align-items: center;
21699
+ gap: 8px;
21700
+ ">
21701
+ <select id="myio-water-tank-display-mode" style="
21702
+ padding: 4px 8px;
21703
+ border: 1px solid #ddd;
21704
+ border-radius: 4px;
21705
+ font-size: 12px;
21706
+ color: #2c3e50;
21707
+ background: white;
21708
+ cursor: pointer;
21709
+ ">
21710
+ <option value="water_level" ${this.chartDisplayMode === "water_level" ? "selected" : ""}>N\xEDvel (m.c.a)</option>
21711
+ <option value="water_percentage" ${this.chartDisplayMode === "water_percentage" ? "selected" : ""}>Percentual (%)</option>
21712
+ </select>
21713
+ <button id="myio-water-tank-maximize" title="Maximizar gr\xE1fico" style="
21714
+ background: #f0f0f0;
21715
+ border: 1px solid #ddd;
21716
+ border-radius: 4px;
21717
+ padding: 4px 8px;
21718
+ cursor: pointer;
21719
+ font-size: 14px;
21720
+ display: flex;
21721
+ align-items: center;
21722
+ justify-content: center;
21723
+ ">\u26F6</button>
21724
+ </div>
21725
+ </div>
21726
+ <div style="flex: 1; min-height: 300px;">
21727
+ <canvas id="myio-water-tank-chart" style="width: 100%; height: 100%;"></canvas>
21728
+ </div>
21729
+ ${firstTs && lastTs ? `
21730
+ <div style="
21731
+ margin-top: 8px;
21732
+ font-size: 11px;
21733
+ color: #7f8c8d;
21734
+ text-align: center;
21735
+ ">
21736
+ ${this.formatDate(firstTs, false)} \u2014 ${this.formatDate(lastTs, false)}
21737
+ (${chartPoints.length} leituras)
21738
+ </div>
21739
+ ` : ""}
21740
+ </div>
21741
+ `;
21742
+ }
21478
21743
  /**
21479
21744
  * Render date range picker
21480
21745
  */
@@ -21495,7 +21760,7 @@ var WaterTankModalView = class {
21495
21760
  flex-wrap: wrap;
21496
21761
  ">
21497
21762
  <div style="display: flex; align-items: center; gap: 8px;">
21498
- <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">From:</label>
21763
+ <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">De:</label>
21499
21764
  <input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
21500
21765
  padding: 8px 12px;
21501
21766
  border: 1px solid #ddd;
@@ -21506,7 +21771,7 @@ var WaterTankModalView = class {
21506
21771
  "/>
21507
21772
  </div>
21508
21773
  <div style="display: flex; align-items: center; gap: 8px;">
21509
- <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">To:</label>
21774
+ <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">At\xE9:</label>
21510
21775
  <input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
21511
21776
  padding: 8px 12px;
21512
21777
  border: 1px solid #ddd;
@@ -21527,7 +21792,7 @@ var WaterTankModalView = class {
21527
21792
  cursor: pointer;
21528
21793
  transition: all 0.2s ease;
21529
21794
  ">
21530
- Apply
21795
+ Aplicar
21531
21796
  </button>
21532
21797
  </div>
21533
21798
  `;
@@ -21749,7 +22014,7 @@ var WaterTankModalView = class {
21749
22014
  }
21750
22015
  const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
21751
22016
  if (applyDatesBtn) {
21752
- applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
22017
+ applyDatesBtn.addEventListener("click", () => this.handleApplyParams());
21753
22018
  }
21754
22019
  const displayModeSelect = this.modal.querySelector("#myio-water-tank-display-mode");
21755
22020
  if (displayModeSelect) {
@@ -21758,6 +22023,10 @@ var WaterTankModalView = class {
21758
22023
  this.refreshChart();
21759
22024
  });
21760
22025
  }
22026
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
22027
+ if (maximizeBtn) {
22028
+ maximizeBtn.addEventListener("click", () => this.handleMaximize());
22029
+ }
21761
22030
  this.overlay.addEventListener("click", (e) => {
21762
22031
  if (e.target === this.overlay) {
21763
22032
  this.config.onClose();
@@ -21772,12 +22041,20 @@ var WaterTankModalView = class {
21772
22041
  });
21773
22042
  }
21774
22043
  /**
21775
- * Handle date range change
22044
+ * Handle date range change (legacy - kept for compatibility)
21776
22045
  */
21777
22046
  handleDateRangeChange() {
22047
+ this.handleApplyParams();
22048
+ }
22049
+ /**
22050
+ * RFC-0107: Handle apply params (date range, aggregation, limit)
22051
+ */
22052
+ handleApplyParams() {
21778
22053
  if (!this.modal) return;
21779
22054
  const startInput = this.modal.querySelector("#myio-water-tank-start-date");
21780
22055
  const endInput = this.modal.querySelector("#myio-water-tank-end-date");
22056
+ const aggregationSelect = this.modal.querySelector("#myio-water-tank-aggregation");
22057
+ const limitSelect = this.modal.querySelector("#myio-water-tank-limit");
21781
22058
  if (startInput && endInput) {
21782
22059
  const startTs = new Date(startInput.value).setHours(0, 0, 0, 0);
21783
22060
  const endTs = new Date(endInput.value).setHours(23, 59, 59, 999);
@@ -21785,17 +22062,77 @@ var WaterTankModalView = class {
21785
22062
  alert("Start date must be before end date");
21786
22063
  return;
21787
22064
  }
21788
- console.log("[WaterTankModalView] Date range changed:", {
22065
+ const aggregation = aggregationSelect?.value || "NONE";
22066
+ const limit = parseInt(limitSelect?.value || "1000", 10);
22067
+ console.log("[WaterTankModalView] Params changed:", {
21789
22068
  startTs,
21790
22069
  endTs,
22070
+ aggregation,
22071
+ limit,
21791
22072
  startDate: new Date(startTs).toISOString(),
21792
22073
  endDate: new Date(endTs).toISOString()
21793
22074
  });
21794
- if (this.config.onDateRangeChange) {
22075
+ this.config.params.startTs = startTs;
22076
+ this.config.params.endTs = endTs;
22077
+ this.config.params.aggregation = aggregation;
22078
+ this.config.params.limit = limit;
22079
+ if (this.config.onParamsChange) {
22080
+ this.config.onParamsChange({ startTs, endTs, aggregation, limit });
22081
+ } else if (this.config.onDateRangeChange) {
21795
22082
  this.config.onDateRangeChange(startTs, endTs);
21796
22083
  }
21797
22084
  }
21798
22085
  }
22086
+ /**
22087
+ * RFC-0107: Handle maximize/restore chart
22088
+ */
22089
+ isMaximized = false;
22090
+ originalModalStyle = "";
22091
+ handleMaximize() {
22092
+ if (!this.modal) return;
22093
+ const chartPanel = this.modal.querySelector("#myio-water-tank-chart-panel");
22094
+ const tankPanel = chartPanel?.previousElementSibling;
22095
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
22096
+ if (!chartPanel) return;
22097
+ if (this.isMaximized) {
22098
+ this.modal.style.cssText = this.originalModalStyle;
22099
+ if (tankPanel) tankPanel.style.display = "";
22100
+ chartPanel.style.cssText = `
22101
+ flex: 1;
22102
+ background: white;
22103
+ border: 1px solid #e0e0e0;
22104
+ border-radius: 12px;
22105
+ padding: 16px;
22106
+ display: flex;
22107
+ flex-direction: column;
22108
+ position: relative;
22109
+ `;
22110
+ if (maximizeBtn) maximizeBtn.textContent = "\u26F6";
22111
+ this.isMaximized = false;
22112
+ } else {
22113
+ this.originalModalStyle = this.modal.style.cssText;
22114
+ this.modal.style.width = "95vw";
22115
+ this.modal.style.height = "90vh";
22116
+ this.modal.style.maxWidth = "95vw";
22117
+ if (tankPanel) tankPanel.style.display = "none";
22118
+ chartPanel.style.cssText = `
22119
+ flex: 1;
22120
+ background: white;
22121
+ border: 1px solid #e0e0e0;
22122
+ border-radius: 12px;
22123
+ padding: 16px;
22124
+ display: flex;
22125
+ flex-direction: column;
22126
+ position: relative;
22127
+ min-height: 100%;
22128
+ `;
22129
+ if (maximizeBtn) maximizeBtn.textContent = "\u26F6";
22130
+ this.isMaximized = true;
22131
+ }
22132
+ requestAnimationFrame(() => {
22133
+ this.renderCanvasChart();
22134
+ });
22135
+ }
21799
22136
  handleEscapeKey(e) {
21800
22137
  if (e.key === "Escape") {
21801
22138
  this.config.onClose();
@@ -21934,7 +22271,7 @@ var WaterTankModalView = class {
21934
22271
  }
21935
22272
  }
21936
22273
  /**
21937
- * Update data and re-render chart
22274
+ * Update data and re-render chart with new layout
21938
22275
  */
21939
22276
  updateData(data) {
21940
22277
  this.config.data = data;
@@ -21942,13 +22279,20 @@ var WaterTankModalView = class {
21942
22279
  const bodyEl = this.modal.querySelector(".myio-water-tank-modal-body");
21943
22280
  if (bodyEl) {
21944
22281
  bodyEl.innerHTML = `
21945
- ${this.renderDateRangePicker()}
21946
- ${this.renderTankVisualization()}
21947
- ${this.renderChart()}
22282
+ ${this.renderControlsBar()}
22283
+ <div style="
22284
+ display: flex;
22285
+ gap: 20px;
22286
+ flex: 1;
22287
+ min-height: 400px;
22288
+ ">
22289
+ ${this.renderTankPanel()}
22290
+ ${this.renderChartPanel()}
22291
+ </div>
21948
22292
  `;
21949
22293
  const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
21950
22294
  if (applyDatesBtn) {
21951
- applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
22295
+ applyDatesBtn.addEventListener("click", () => this.handleApplyParams());
21952
22296
  }
21953
22297
  const displayModeSelect = this.modal.querySelector("#myio-water-tank-display-mode");
21954
22298
  if (displayModeSelect) {
@@ -21957,6 +22301,10 @@ var WaterTankModalView = class {
21957
22301
  this.refreshChart();
21958
22302
  });
21959
22303
  }
22304
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
22305
+ if (maximizeBtn) {
22306
+ maximizeBtn.addEventListener("click", () => this.handleMaximize());
22307
+ }
21960
22308
  requestAnimationFrame(() => {
21961
22309
  this.renderCanvasChart();
21962
22310
  });
@@ -22236,7 +22584,8 @@ var WaterTankModal = class {
22236
22584
  onError: (error) => this.handleError(error),
22237
22585
  onClose: () => this.close(),
22238
22586
  // Call close() to destroy view and trigger user callback
22239
- onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs)
22587
+ onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs),
22588
+ onParamsChange: (params) => this.handleParamsChange(params)
22240
22589
  });
22241
22590
  this.view.render();
22242
22591
  this.view.show();
@@ -22301,6 +22650,43 @@ var WaterTankModal = class {
22301
22650
  this.handleError(error);
22302
22651
  }
22303
22652
  }
22653
+ /**
22654
+ * RFC-0107: Handle params change (date range, aggregation, limit)
22655
+ */
22656
+ async handleParamsChange(params) {
22657
+ console.log("[WaterTankModal] Params changed:", {
22658
+ startTs: params.startTs,
22659
+ endTs: params.endTs,
22660
+ aggregation: params.aggregation,
22661
+ limit: params.limit,
22662
+ startDate: new Date(params.startTs).toISOString(),
22663
+ endDate: new Date(params.endTs).toISOString()
22664
+ });
22665
+ this.options.startTs = params.startTs;
22666
+ this.options.endTs = params.endTs;
22667
+ this.options.aggregation = params.aggregation;
22668
+ this.options.limit = params.limit;
22669
+ this.context.timeRange.startTs = params.startTs;
22670
+ this.context.timeRange.endTs = params.endTs;
22671
+ try {
22672
+ console.log("[WaterTankModal] Fetching data with new params...");
22673
+ this.data = await this.fetchTelemetryData();
22674
+ if (this.view) {
22675
+ this.view.updateData(this.data);
22676
+ }
22677
+ if (this.options.onDataLoaded) {
22678
+ try {
22679
+ this.options.onDataLoaded(this.data);
22680
+ } catch (callbackError) {
22681
+ console.warn("[WaterTankModal] onDataLoaded callback error:", callbackError);
22682
+ }
22683
+ }
22684
+ console.log("[WaterTankModal] Data refreshed with new params successfully");
22685
+ } catch (error) {
22686
+ console.error("[WaterTankModal] Failed to fetch data with new params:", error);
22687
+ this.handleError(error);
22688
+ }
22689
+ }
22304
22690
  /**
22305
22691
  * Handle export functionality
22306
22692
  */
@@ -22422,8 +22808,8 @@ function validateOptions2(options) {
22422
22808
  }
22423
22809
  if (options.currentLevel !== void 0) {
22424
22810
  const level = Number(options.currentLevel);
22425
- if (isNaN(level) || level < 0 || level > 100) {
22426
- errors.push("currentLevel must be a number between 0 and 100");
22811
+ if (isNaN(level) || level < 0) {
22812
+ errors.push("currentLevel must be a non-negative number");
22427
22813
  }
22428
22814
  }
22429
22815
  if (options.limit !== void 0) {
@@ -28648,13 +29034,31 @@ var DefaultSettingsPersister = class {
28648
29034
  };
28649
29035
 
28650
29036
  // src/components/premium-modals/settings/SettingsFetcher.ts
28651
- var DefaultSettingsFetcher = class {
29037
+ var DefaultSettingsFetcher = class _DefaultSettingsFetcher {
28652
29038
  jwtToken;
28653
29039
  tbBaseUrl;
29040
+ static FETCH_TIMEOUT_MS = 8e3;
29041
+ // 8 second timeout
28654
29042
  constructor(jwtToken, apiConfig) {
28655
29043
  this.jwtToken = jwtToken;
28656
29044
  this.tbBaseUrl = apiConfig?.tbBaseUrl || window.location.origin;
28657
29045
  }
29046
+ /**
29047
+ * Fetch with timeout to prevent hanging requests from blocking modal render
29048
+ */
29049
+ async fetchWithTimeout(url, options, timeoutMs = _DefaultSettingsFetcher.FETCH_TIMEOUT_MS) {
29050
+ const controller = new AbortController();
29051
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
29052
+ try {
29053
+ const response = await fetch(url, {
29054
+ ...options,
29055
+ signal: controller.signal
29056
+ });
29057
+ return response;
29058
+ } finally {
29059
+ clearTimeout(timeoutId);
29060
+ }
29061
+ }
28658
29062
  async fetchCurrentSettings(deviceId, jwtToken, scope = "SERVER_SCOPE") {
28659
29063
  try {
28660
29064
  const [entityResult, attributesResult] = await Promise.allSettled([
@@ -28688,9 +29092,12 @@ var DefaultSettingsFetcher = class {
28688
29092
  }
28689
29093
  }
28690
29094
  async fetchDeviceEntity(deviceId) {
28691
- const response = await fetch(`${this.tbBaseUrl}/api/device/${deviceId}`, {
28692
- headers: { "X-Authorization": `Bearer ${this.jwtToken}` }
28693
- });
29095
+ const response = await this.fetchWithTimeout(
29096
+ `${this.tbBaseUrl}/api/device/${deviceId}`,
29097
+ {
29098
+ headers: { "X-Authorization": `Bearer ${this.jwtToken}` }
29099
+ }
29100
+ );
28694
29101
  if (!response.ok) {
28695
29102
  throw new Error(
28696
29103
  `Failed to fetch device entity: ${response.status} ${response.statusText}`
@@ -28702,7 +29109,7 @@ var DefaultSettingsFetcher = class {
28702
29109
  };
28703
29110
  }
28704
29111
  async fetchDeviceAttributes(deviceId, scope) {
28705
- const response = await fetch(
29112
+ const response = await this.fetchWithTimeout(
28706
29113
  `${this.tbBaseUrl}/api/plugins/telemetry/DEVICE/${deviceId}/values/attributes/${scope}`,
28707
29114
  {
28708
29115
  headers: { "X-Authorization": `Bearer ${this.jwtToken}` }
@@ -28911,12 +29318,16 @@ var SettingsController = class {
28911
29318
  const tbBaseUrl = this.params.api?.tbBaseUrl || window.location.origin;
28912
29319
  const url = `${tbBaseUrl}/api/plugins/telemetry/CUSTOMER/${customerId}/values/attributes/SERVER_SCOPE?keys=mapInstantaneousPower`;
28913
29320
  console.log("[SettingsModal] RFC-0080: Fetching GLOBAL from:", url);
29321
+ const controller = new AbortController();
29322
+ const timeoutId = setTimeout(() => controller.abort(), 8e3);
28914
29323
  const response = await fetch(url, {
28915
29324
  headers: {
28916
29325
  "X-Authorization": `Bearer ${jwtToken}`,
28917
29326
  "Content-Type": "application/json"
28918
- }
29327
+ },
29328
+ signal: controller.signal
28919
29329
  });
29330
+ clearTimeout(timeoutId);
28920
29331
  if (!response.ok) {
28921
29332
  throw new Error(`HTTP ${response.status}`);
28922
29333
  }