myio-js-library 0.1.539 → 0.1.540

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.cjs CHANGED
@@ -155,6 +155,7 @@ __export(src_exports, {
155
155
  DeviceOperationalCardGridController: () => DeviceOperationalCardGridController,
156
156
  DeviceOperationalCardGridView: () => DeviceOperationalCardGridView,
157
157
  DeviceOperationalCardView: () => DeviceOperationalCardView,
158
+ DeviceProfileTab: () => DeviceProfileTab,
158
159
  DeviceStatusType: () => DeviceStatusType,
159
160
  DomainType: () => DomainType,
160
161
  ENERGY_SORT_OPTIONS: () => ENERGY_SORT_OPTIONS,
@@ -890,7 +891,7 @@ module.exports = __toCommonJS(src_exports);
890
891
  // package.json
891
892
  var package_default = {
892
893
  name: "myio-js-library",
893
- version: "0.1.539",
894
+ version: "0.1.540",
894
895
  description: "A clean, standalone JS SDK for MYIO projects",
895
896
  license: "MIT",
896
897
  repository: "github:gh-myio/myio-js-library",
@@ -60308,6 +60309,517 @@ var ExclusionGroupsTab = class {
60308
60309
  }
60309
60310
  };
60310
60311
 
60312
+ // src/components/premium-modals/settings/device-profile/DeviceProfileTab.ts
60313
+ var TB_NULL_CUSTOMER_ID = "13814000-1dd2-11b2-8080-808080808080";
60314
+ var STYLES_ID3 = "dpt-styles";
60315
+ var EDITABLE_ATTRS = [
60316
+ { key: "deviceProfile", label: "Device Profile" },
60317
+ { key: "identifier", label: "Identifier" },
60318
+ { key: "gcdrDeviceId", label: "GCDR Device ID" },
60319
+ { key: "gcdrCustomerId", label: "GCDR Customer ID" },
60320
+ { key: "gcdrAssetId", label: "GCDR Asset ID" }
60321
+ ];
60322
+ var READONLY_ATTRS = [
60323
+ { key: "deviceType", label: "Device Type" },
60324
+ { key: "slaveId", label: "Slave ID" },
60325
+ { key: "centralId", label: "Central ID" },
60326
+ { key: "centralName", label: "Central" }
60327
+ ];
60328
+ var DeviceProfileTab = class {
60329
+ config;
60330
+ entity = null;
60331
+ attributes = [];
60332
+ ownerName = null;
60333
+ deviceProfileOptions = null;
60334
+ customerOptions = null;
60335
+ constructor(config2) {
60336
+ this.config = config2;
60337
+ }
60338
+ async init() {
60339
+ this.config.container.innerHTML = this.getLoadingHTML();
60340
+ this.injectStyles();
60341
+ await this.loadAll();
60342
+ this.renderAndAttach();
60343
+ }
60344
+ destroy() {
60345
+ }
60346
+ // --------------------------------------------------------------------------
60347
+ // Private — data
60348
+ // --------------------------------------------------------------------------
60349
+ headers(extra) {
60350
+ return { "X-Authorization": `Bearer ${this.config.jwtToken}`, ...extra || {} };
60351
+ }
60352
+ async loadAll() {
60353
+ const [entity, attributes] = await Promise.all([this.fetchEntity(), this.fetchAttributes()]);
60354
+ this.entity = entity;
60355
+ this.attributes = attributes;
60356
+ const customerId = entity?.customerId?.id;
60357
+ this.ownerName = customerId && customerId !== TB_NULL_CUSTOMER_ID ? await this.fetchCustomerName(customerId) : null;
60358
+ }
60359
+ async fetchEntity() {
60360
+ try {
60361
+ const { tbBaseUrl, deviceId } = this.config;
60362
+ const res = await fetch(`${tbBaseUrl}/api/device/${deviceId}`, { headers: this.headers() });
60363
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60364
+ return await res.json();
60365
+ } catch (e) {
60366
+ console.warn("[DeviceProfileTab] Failed to fetch device entity:", e);
60367
+ return null;
60368
+ }
60369
+ }
60370
+ async fetchAttributes() {
60371
+ try {
60372
+ const { tbBaseUrl, deviceId } = this.config;
60373
+ const res = await fetch(
60374
+ `${tbBaseUrl}/api/plugins/telemetry/DEVICE/${deviceId}/values/attributes/SERVER_SCOPE`,
60375
+ { headers: this.headers() }
60376
+ );
60377
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60378
+ const raw = await res.json();
60379
+ return Array.isArray(raw) ? raw : [];
60380
+ } catch (e) {
60381
+ console.warn("[DeviceProfileTab] Failed to fetch device attributes:", e);
60382
+ return [];
60383
+ }
60384
+ }
60385
+ async fetchCustomerName(customerId) {
60386
+ try {
60387
+ const res = await fetch(`${this.config.tbBaseUrl}/api/customer/${customerId}`, {
60388
+ headers: this.headers()
60389
+ });
60390
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60391
+ const c = await res.json();
60392
+ return c?.title || c?.name || null;
60393
+ } catch (e) {
60394
+ console.warn("[DeviceProfileTab] Failed to fetch customer name:", e);
60395
+ return null;
60396
+ }
60397
+ }
60398
+ async fetchDeviceProfileOptions() {
60399
+ if (this.deviceProfileOptions) return this.deviceProfileOptions;
60400
+ try {
60401
+ const res = await fetch(`${this.config.tbBaseUrl}/api/deviceProfile/names?activeOnly=true`, {
60402
+ headers: this.headers()
60403
+ });
60404
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60405
+ const raw = await res.json();
60406
+ this.deviceProfileOptions = raw.map((p) => ({ id: p.id.id, name: p.name }));
60407
+ } catch (e) {
60408
+ console.warn("[DeviceProfileTab] Failed to fetch device profiles:", e);
60409
+ this.deviceProfileOptions = [];
60410
+ }
60411
+ return this.deviceProfileOptions;
60412
+ }
60413
+ async fetchCustomerOptions() {
60414
+ if (this.customerOptions) return this.customerOptions;
60415
+ try {
60416
+ const res = await fetch(
60417
+ `${this.config.tbBaseUrl}/api/customers?pageSize=200&page=0&sortProperty=title&sortOrder=ASC`,
60418
+ { headers: this.headers() }
60419
+ );
60420
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60421
+ const json = await res.json();
60422
+ const data = json?.data || [];
60423
+ this.customerOptions = data.map((c) => ({ id: c.id.id, title: c.title }));
60424
+ } catch (e) {
60425
+ console.warn("[DeviceProfileTab] Failed to fetch customers:", e);
60426
+ this.customerOptions = [];
60427
+ }
60428
+ return this.customerOptions;
60429
+ }
60430
+ attrValue(key) {
60431
+ return this.attributes.find((a) => a.key === key)?.value;
60432
+ }
60433
+ // --------------------------------------------------------------------------
60434
+ // Private — writes (one call per field, mirroring openUpsellModal.ts 1:1)
60435
+ // --------------------------------------------------------------------------
60436
+ /** `POST .../attributes/SERVER_SCOPE` — same call as the bulk "Forçar Atributo" tool. */
60437
+ async saveAttribute(key, value) {
60438
+ const { tbBaseUrl, deviceId } = this.config;
60439
+ const res = await fetch(`${tbBaseUrl}/api/plugins/telemetry/DEVICE/${deviceId}/attributes/SERVER_SCOPE`, {
60440
+ method: "POST",
60441
+ headers: this.headers({ "Content-Type": "application/json" }),
60442
+ body: JSON.stringify({ [key]: value })
60443
+ });
60444
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60445
+ const idx = this.attributes.findIndex((a) => a.key === key);
60446
+ if (idx >= 0) this.attributes[idx] = { key, value };
60447
+ else this.attributes.push({ key, value });
60448
+ }
60449
+ /** Fetch → patch one field → `POST /api/device` (same pattern as saveEntityLabel/changeDeviceProfile). */
60450
+ async saveEntityField(patch) {
60451
+ const { tbBaseUrl, deviceId } = this.config;
60452
+ const getRes = await fetch(`${tbBaseUrl}/api/device/${deviceId}`, { headers: this.headers() });
60453
+ if (!getRes.ok) throw new Error(`HTTP ${getRes.status}`);
60454
+ const device = await getRes.json();
60455
+ const postRes = await fetch(`${tbBaseUrl}/api/device`, {
60456
+ method: "POST",
60457
+ headers: this.headers({ "Content-Type": "application/json" }),
60458
+ body: JSON.stringify({ ...device, ...patch })
60459
+ });
60460
+ if (!postRes.ok) throw new Error(`HTTP ${postRes.status}`);
60461
+ this.entity = { ...this.entity, ...patch };
60462
+ }
60463
+ /** `POST /api/owner/CUSTOMER/{id}/DEVICE/{id}` with CE fallback — same as changeDeviceOwner(). */
60464
+ async saveOwner(newCustomerId) {
60465
+ const { tbBaseUrl, deviceId } = this.config;
60466
+ const peRes = await fetch(`${tbBaseUrl}/api/owner/CUSTOMER/${newCustomerId}/DEVICE/${deviceId}`, {
60467
+ method: "POST",
60468
+ headers: this.headers({ "Content-Type": "application/json" }),
60469
+ body: JSON.stringify([])
60470
+ });
60471
+ if (!peRes.ok) {
60472
+ if (peRes.status !== 404) throw new Error(`HTTP ${peRes.status}`);
60473
+ const ceRes = await fetch(`${tbBaseUrl}/api/customer/${newCustomerId}/device/${deviceId}`, {
60474
+ method: "POST",
60475
+ headers: this.headers({ "Content-Type": "application/json" }),
60476
+ body: JSON.stringify({})
60477
+ });
60478
+ if (!ceRes.ok) throw new Error(`HTTP ${ceRes.status}`);
60479
+ }
60480
+ this.entity = { ...this.entity, customerId: { id: newCustomerId, entityType: "CUSTOMER" } };
60481
+ this.ownerName = await this.fetchCustomerName(newCustomerId);
60482
+ }
60483
+ /**
60484
+ * Single-device "Sync Ingestion ID" — identical match algorithm to the bulk
60485
+ * tool (customer's `ingestionId` attr → this device's centralId+slaveId →
60486
+ * paged ingestion-API device list → match by gateway hardwareUuid+slaveId),
60487
+ * scoped to one device. Reuses the same ingestion-API app credentials
60488
+ * already shipped in openUpsellModal.ts (not a new secret).
60489
+ */
60490
+ async syncIngestionId() {
60491
+ const INGESTION_AUTH_URL2 = "https://api.data.apps.myio-bas.com/api/v1/auth";
60492
+ const INGESTION_API_BASE2 = "https://api.data.apps.myio-bas.com/api/v1";
60493
+ const INGESTION_CLIENT_ID2 = "myioadmi_mekj7xw7_sccibe";
60494
+ const INGESTION_CLIENT_SECRET2 = "KmXhNZu0uydeWZ8scAi43h7P2pntGoWkdzNVMSjbVj3slEsZ5hGVXyayshgJAoqA";
60495
+ const centralId = this.attrValue("centralId");
60496
+ const slaveId = this.attrValue("slaveId");
60497
+ if (!centralId || slaveId === void 0 || slaveId === null || slaveId === "") {
60498
+ return { ok: false, message: "Device sem centralId/slaveId \u2014 n\xE3o \xE9 poss\xEDvel localizar na Ingestion API." };
60499
+ }
60500
+ const customerId = this.entity?.customerId?.id;
60501
+ if (!customerId || customerId === TB_NULL_CUSTOMER_ID) {
60502
+ return { ok: false, message: "Device sem Owner (customer) \u2014 n\xE3o \xE9 poss\xEDvel resolver o customer da Ingestion API." };
60503
+ }
60504
+ const customerAttrs = await this.fetchCustomerAttrs(customerId);
60505
+ const ingestionCustomerId = customerAttrs.find((a) => a.key === "ingestionId")?.value;
60506
+ if (!ingestionCustomerId) {
60507
+ return { ok: false, message: "Customer n\xE3o tem atributo ingestionId configurado no ThingsBoard." };
60508
+ }
60509
+ const authRes = await fetch(INGESTION_AUTH_URL2, {
60510
+ method: "POST",
60511
+ headers: { "Content-Type": "application/json" },
60512
+ body: JSON.stringify({ client_id: INGESTION_CLIENT_ID2, client_secret: INGESTION_CLIENT_SECRET2 })
60513
+ });
60514
+ if (!authRes.ok) throw new Error(`Ingestion auth HTTP ${authRes.status}`);
60515
+ const authJson = await authRes.json();
60516
+ const token = authJson?.access_token;
60517
+ if (!token) throw new Error("Ingestion auth: access_token ausente na resposta.");
60518
+ const slaveIdNum = typeof slaveId === "string" ? parseInt(slaveId, 10) : slaveId;
60519
+ let page = 1;
60520
+ let match = null;
60521
+ for (; page <= 50 && !match; page++) {
60522
+ const res = await fetch(
60523
+ `${INGESTION_API_BASE2}/management/devices?page=${page}&limit=100&customerId=${encodeURIComponent(
60524
+ ingestionCustomerId
60525
+ )}&includeInactive=false&sortBy=name&sortOrder=asc`,
60526
+ { headers: { Authorization: `Bearer ${token}` } }
60527
+ );
60528
+ if (!res.ok) throw new Error(`Ingestion devices HTTP ${res.status}`);
60529
+ const json = await res.json();
60530
+ const list = json?.data || [];
60531
+ for (const d of list) {
60532
+ const gwId = d.gateway?.hardwareUuid || d.gatewayId || d.gateway?.id;
60533
+ if (gwId === centralId && d.slaveId === slaveIdNum) {
60534
+ match = { id: d.id };
60535
+ break;
60536
+ }
60537
+ }
60538
+ const totalPages = json?.pagination?.pages || 1;
60539
+ if (page >= totalPages) break;
60540
+ }
60541
+ if (!match) {
60542
+ return { ok: false, message: `Nenhum device na Ingestion API com centralId=${centralId} e slaveId=${slaveIdNum}.` };
60543
+ }
60544
+ await this.saveAttribute("ingestionId", match.id);
60545
+ return { ok: true, message: `ingestionId atualizado: ${match.id}` };
60546
+ }
60547
+ async fetchCustomerAttrs(customerId) {
60548
+ const res = await fetch(
60549
+ `${this.config.tbBaseUrl}/api/plugins/telemetry/CUSTOMER/${customerId}/values/attributes/SERVER_SCOPE`,
60550
+ { headers: this.headers() }
60551
+ );
60552
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
60553
+ const raw = await res.json();
60554
+ return Array.isArray(raw) ? raw : [];
60555
+ }
60556
+ // --------------------------------------------------------------------------
60557
+ // Private — render
60558
+ // --------------------------------------------------------------------------
60559
+ esc(s) {
60560
+ return s.replace(
60561
+ /[&<>"']/g,
60562
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
60563
+ );
60564
+ }
60565
+ displayText(value) {
60566
+ if (value === void 0 || value === null || value === "") return "\u2014";
60567
+ if (typeof value === "object") {
60568
+ try {
60569
+ return JSON.stringify(value);
60570
+ } catch {
60571
+ return String(value);
60572
+ }
60573
+ }
60574
+ return String(value);
60575
+ }
60576
+ formatDate(ts) {
60577
+ if (!ts) return "\u2014";
60578
+ try {
60579
+ return new Date(ts).toLocaleString("pt-BR");
60580
+ } catch {
60581
+ return String(ts);
60582
+ }
60583
+ }
60584
+ getLoadingHTML() {
60585
+ return `
60586
+ <div style="padding: 20px; text-align: center; color: #6c757d;">
60587
+ <div class="loading-spinner"></div>
60588
+ <p>Carregando perfil do dispositivo...</p>
60589
+ </div>`;
60590
+ }
60591
+ /** One editable/read-only key-value row. `kind` picks the edit widget. */
60592
+ renderRow(field, label, value, kind) {
60593
+ const text2 = this.esc(this.displayText(value));
60594
+ if (kind === "readonly") {
60595
+ return `
60596
+ <div class="dpt-row" data-field="${this.esc(field)}">
60597
+ <span class="dpt-label">${this.esc(label)}</span>
60598
+ <span class="dpt-value">${text2}</span>
60599
+ </div>`;
60600
+ }
60601
+ const syncBtn = kind === "ingestion" ? `<button type="button" class="dpt-icon-btn" data-action="sync-ingestion" title="Buscar ingestionId na Ingestion API">\u{1F504}</button>` : "";
60602
+ return `
60603
+ <div class="dpt-row dpt-row--editable" data-field="${this.esc(field)}" data-kind="${kind}">
60604
+ <span class="dpt-label">${this.esc(label)}</span>
60605
+ <span class="dpt-value-wrap">
60606
+ <span class="dpt-value" data-view>${text2}</span>
60607
+ ${syncBtn}
60608
+ <button type="button" class="dpt-icon-btn" data-action="edit" title="Editar">\u270F\uFE0F</button>
60609
+ </span>
60610
+ </div>`;
60611
+ }
60612
+ renderTab() {
60613
+ const e = this.entity;
60614
+ const ownerId = e?.customerId?.id;
60615
+ const ownerDisplay = ownerId && ownerId !== TB_NULL_CUSTOMER_ID ? this.ownerName || ownerId : "\u2014 (sem customer)";
60616
+ const highlightedRows = [
60617
+ this.renderRow("type", "Type (TB)", e?.type, "entity-profile"),
60618
+ this.renderRow("label", "Label", e?.label, "entity-label"),
60619
+ this.renderRow("owner", "Owner", ownerDisplay, "owner")
60620
+ ].join("");
60621
+ const editableAttrRows = EDITABLE_ATTRS.map(
60622
+ ({ key, label }) => key === "ingestionId" ? "" : this.renderRow(key, label, this.attrValue(key), "attr-text")
60623
+ ).join("");
60624
+ const ingestionRow = this.renderRow("ingestionId", "Ingestion ID", this.attrValue("ingestionId"), "ingestion");
60625
+ const readonlyRows = READONLY_ATTRS.map(
60626
+ ({ key, label }) => this.renderRow(key, label, this.attrValue(key), "readonly")
60627
+ ).join("");
60628
+ const highlightedKeys = /* @__PURE__ */ new Set([...EDITABLE_ATTRS.map((a) => a.key), ...READONLY_ATTRS.map((a) => a.key)]);
60629
+ const otherAttrs = this.attributes.filter((a) => !highlightedKeys.has(a.key)).sort((a, b) => a.key.localeCompare(b.key));
60630
+ const otherRows = otherAttrs.length ? otherAttrs.map((a) => this.renderRow(a.key, a.key, a.value, "readonly")).join("") : `<div class="dpt-empty">Nenhum outro atributo SERVER_SCOPE.</div>`;
60631
+ return `
60632
+ <div class="dpt-tab">
60633
+ <div class="dpt-notice">\u{1F512} Aba vis\xEDvel apenas para administradores MyIO \u2014 altera dados direto no ThingsBoard.</div>
60634
+
60635
+ <div class="dpt-section">
60636
+ <div class="dpt-section-title">Identifica\xE7\xE3o (ThingsBoard)</div>
60637
+ <div class="dpt-card">
60638
+ ${highlightedRows}
60639
+ <div class="dpt-row"><span class="dpt-label">Nome</span><span class="dpt-value">${this.esc(this.displayText(e?.name))}</span></div>
60640
+ <div class="dpt-row"><span class="dpt-label">Device Profile (TB)</span><span class="dpt-value">${this.esc(this.displayText(e?.deviceProfileName))}</span></div>
60641
+ <div class="dpt-row"><span class="dpt-label">ID</span><span class="dpt-value dpt-mono">${this.esc(this.displayText(e?.id?.id || this.config.deviceId))}</span></div>
60642
+ <div class="dpt-row"><span class="dpt-label">Criado em</span><span class="dpt-value">${this.formatDate(e?.createdTime)}</span></div>
60643
+ </div>
60644
+ </div>
60645
+
60646
+ <div class="dpt-section">
60647
+ <div class="dpt-section-title">Classifica\xE7\xE3o (atributos da aplica\xE7\xE3o)</div>
60648
+ <div class="dpt-card">
60649
+ ${editableAttrRows}
60650
+ ${ingestionRow}
60651
+ ${readonlyRows}
60652
+ </div>
60653
+ </div>
60654
+
60655
+ <div class="dpt-section">
60656
+ <div class="dpt-section-title">Todos os atributos SERVER_SCOPE (${this.attributes.length})</div>
60657
+ <div class="dpt-card dpt-card--scroll">
60658
+ ${otherRows}
60659
+ </div>
60660
+ </div>
60661
+ </div>`;
60662
+ }
60663
+ renderAndAttach() {
60664
+ this.config.container.innerHTML = this.renderTab();
60665
+ this.attachListeners();
60666
+ }
60667
+ // --------------------------------------------------------------------------
60668
+ // Private — interaction
60669
+ // --------------------------------------------------------------------------
60670
+ attachListeners() {
60671
+ const root = this.config.container;
60672
+ root.addEventListener("click", (ev) => this.onClick(ev));
60673
+ }
60674
+ async onClick(ev) {
60675
+ const target = ev.target;
60676
+ const btn = target.closest("button[data-action]");
60677
+ if (!btn) return;
60678
+ const row = btn.closest(".dpt-row");
60679
+ if (!row) return;
60680
+ const field = row.dataset.field || "";
60681
+ const kind = row.dataset.kind || "";
60682
+ const action = btn.dataset.action;
60683
+ if (action === "sync-ingestion") {
60684
+ await this.handleSyncIngestion(row, btn);
60685
+ return;
60686
+ }
60687
+ if (action === "edit") {
60688
+ await this.beginEdit(row, field, kind);
60689
+ return;
60690
+ }
60691
+ if (action === "save") {
60692
+ await this.commitEdit(row, field, kind);
60693
+ return;
60694
+ }
60695
+ if (action === "cancel") {
60696
+ this.renderAndAttach();
60697
+ }
60698
+ }
60699
+ async handleSyncIngestion(row, btn) {
60700
+ btn.disabled = true;
60701
+ const original = btn.textContent;
60702
+ btn.textContent = "\u23F3";
60703
+ try {
60704
+ const result = await this.syncIngestionId();
60705
+ if (result.ok) {
60706
+ this.renderAndAttach();
60707
+ } else {
60708
+ alert(result.message);
60709
+ }
60710
+ } catch (e) {
60711
+ alert("Erro ao sincronizar ingestionId: " + (e instanceof Error ? e.message : String(e)));
60712
+ } finally {
60713
+ btn.disabled = false;
60714
+ btn.textContent = original;
60715
+ }
60716
+ }
60717
+ async beginEdit(row, field, kind) {
60718
+ const wrap = row.querySelector(".dpt-value-wrap");
60719
+ if (!wrap) return;
60720
+ if (kind === "entity-profile") {
60721
+ const options = await this.fetchDeviceProfileOptions();
60722
+ const currentId = this.entity?.deviceProfileId?.id || "";
60723
+ wrap.innerHTML = this.editorHtml(
60724
+ `<select class="dpt-input" data-edit>
60725
+ ${options.map((o) => `<option value="${this.esc(o.id)}" ${o.id === currentId ? "selected" : ""}>${this.esc(o.name)}</option>`).join("")}
60726
+ </select>`
60727
+ );
60728
+ } else if (kind === "owner") {
60729
+ const options = await this.fetchCustomerOptions();
60730
+ const currentId = this.entity?.customerId?.id || "";
60731
+ wrap.innerHTML = this.editorHtml(
60732
+ `<select class="dpt-input" data-edit>
60733
+ <option value="">\u2014 selecione \u2014</option>
60734
+ ${options.map((o) => `<option value="${this.esc(o.id)}" ${o.id === currentId ? "selected" : ""}>${this.esc(o.title)}</option>`).join("")}
60735
+ </select>`
60736
+ );
60737
+ } else {
60738
+ const currentValue = kind === "entity-label" ? this.entity?.label || "" : this.attrValue(field) || "";
60739
+ wrap.innerHTML = this.editorHtml(
60740
+ `<input class="dpt-input" data-edit type="text" value="${this.esc(String(currentValue))}" />`
60741
+ );
60742
+ const input = wrap.querySelector("input[data-edit]");
60743
+ input?.focus();
60744
+ input?.select();
60745
+ }
60746
+ }
60747
+ editorHtml(inputHtml) {
60748
+ return `
60749
+ ${inputHtml}
60750
+ <button type="button" class="dpt-icon-btn" data-action="save" title="Salvar">\u2714\uFE0F</button>
60751
+ <button type="button" class="dpt-icon-btn" data-action="cancel" title="Cancelar">\u2716\uFE0F</button>
60752
+ `;
60753
+ }
60754
+ async commitEdit(row, field, kind) {
60755
+ const editEl = row.querySelector("[data-edit]");
60756
+ if (!editEl) return;
60757
+ const value = editEl.value.trim();
60758
+ const saveBtn = row.querySelector('button[data-action="save"]');
60759
+ if (saveBtn) saveBtn.disabled = true;
60760
+ try {
60761
+ if (kind === "entity-label") {
60762
+ await this.saveEntityField({ label: value });
60763
+ } else if (kind === "entity-profile") {
60764
+ if (!value) throw new Error("Selecione um Device Profile.");
60765
+ await this.saveEntityField({ deviceProfileId: { id: value, entityType: "DEVICE_PROFILE" } });
60766
+ } else if (kind === "owner") {
60767
+ if (!value) throw new Error("Selecione um Owner.");
60768
+ await this.saveOwner(value);
60769
+ } else {
60770
+ await this.saveAttribute(field, value);
60771
+ }
60772
+ this.renderAndAttach();
60773
+ } catch (e) {
60774
+ alert("Erro ao salvar: " + (e instanceof Error ? e.message : String(e)));
60775
+ } finally {
60776
+ if (saveBtn) saveBtn.disabled = false;
60777
+ }
60778
+ }
60779
+ injectStyles() {
60780
+ if (document.getElementById(STYLES_ID3)) return;
60781
+ const style = document.createElement("style");
60782
+ style.id = STYLES_ID3;
60783
+ style.textContent = `
60784
+ .dpt-tab { padding: 4px 2px 20px; }
60785
+ .dpt-notice {
60786
+ background: #fef3c7; border: 1px solid #f59e0b; color: #92400e;
60787
+ border-radius: 8px; padding: 8px 12px; font-size: 12px; margin-bottom: 16px;
60788
+ }
60789
+ .dpt-section { margin-bottom: 20px; }
60790
+ .dpt-section-title {
60791
+ font-size: 12px; font-weight: 700; text-transform: uppercase;
60792
+ letter-spacing: 0.6px; color: #6b7280; margin-bottom: 8px;
60793
+ }
60794
+ .dpt-card {
60795
+ background: #fff; border: 1px solid #e5e7eb; border-radius: 10px;
60796
+ padding: 4px 16px;
60797
+ }
60798
+ .dpt-card--scroll { max-height: 280px; overflow-y: auto; }
60799
+ .dpt-row {
60800
+ display: flex; align-items: center; justify-content: space-between;
60801
+ gap: 12px; padding: 9px 0; border-bottom: 1px solid #f1f5f9;
60802
+ }
60803
+ .dpt-row:last-child { border-bottom: none; }
60804
+ .dpt-label { font-size: 12px; color: #6b7280; font-weight: 600; white-space: nowrap; }
60805
+ .dpt-value { font-size: 13px; color: #111827; text-align: right; word-break: break-word; }
60806
+ .dpt-value-wrap { display: flex; align-items: center; gap: 6px; }
60807
+ .dpt-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
60808
+ .dpt-empty { padding: 16px 0; text-align: center; color: #9ca3af; font-size: 13px; }
60809
+ .dpt-icon-btn {
60810
+ border: none; background: transparent; cursor: pointer; font-size: 13px;
60811
+ padding: 2px 4px; border-radius: 6px; line-height: 1;
60812
+ }
60813
+ .dpt-icon-btn:hover { background: #f3f4f6; }
60814
+ .dpt-input {
60815
+ font-size: 12px; padding: 4px 6px; border: 1px solid #d1d5db; border-radius: 6px;
60816
+ max-width: 220px;
60817
+ }
60818
+ `;
60819
+ document.head.appendChild(style);
60820
+ }
60821
+ };
60822
+
60311
60823
  // src/services/freshdesk/FreshdeskClient.ts
60312
60824
  var FreshdeskClient_exports = {};
60313
60825
  __export(FreshdeskClient_exports, {
@@ -61891,6 +62403,7 @@ var SettingsModalView = class _SettingsModalView {
61891
62403
  chamadosTabHandle = null;
61892
62404
  // Exclusão de Grupos tab
61893
62405
  exclusionGroupsTab = null;
62406
+ deviceProfileTab = null;
61894
62407
  currentTab = "general";
61895
62408
  currentUser = null;
61896
62409
  permissions = null;
@@ -61969,6 +62482,7 @@ var SettingsModalView = class _SettingsModalView {
61969
62482
  this.initAlarmsTab();
61970
62483
  this.initChamadosTab();
61971
62484
  this.initExclusionGroupsTab();
62485
+ this.initDeviceProfileTab();
61972
62486
  if (this.config.isGateway && this.isSuperAdmin()) {
61973
62487
  this.bindIncidentsTabEvents();
61974
62488
  this.initIncidentsDateRange();
@@ -62184,6 +62698,44 @@ var SettingsModalView = class _SettingsModalView {
62184
62698
  container.innerHTML = '<p style="color:#dc3545;padding:20px;text-align:center;">Erro ao carregar configura\xE7\xF5es de exclus\xE3o</p>';
62185
62699
  }
62186
62700
  }
62701
+ // Perfil de Dispositivo: ThingsBoard identity + SERVER_SCOPE attribute dump,
62702
+ // with inline edit/force for the fields support needs to fix — MyIO admins
62703
+ // only (writes ThingsBoard entity/attributes/owner directly).
62704
+ async initDeviceProfileTab() {
62705
+ if (!this.isSuperAdmin()) {
62706
+ this.modal.querySelector('[data-tab="device-profile"]')?.remove();
62707
+ return;
62708
+ }
62709
+ const container = this.modal.querySelector("#device-profile-tab-content");
62710
+ if (!container) return;
62711
+ const { deviceId, jwtToken, tbBaseUrl } = this.config;
62712
+ if (!deviceId || !jwtToken) {
62713
+ container.innerHTML = `
62714
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
62715
+ min-height:320px;padding:40px 24px;text-align:center;">
62716
+ <div style="font-size:15px;font-weight:600;color:#374151;margin-bottom:8px;">
62717
+ Perfil de dispositivo n\xE3o dispon\xEDvel
62718
+ </div>
62719
+ <div style="font-size:13px;color:#9ca3af;max-width:320px;line-height:1.6;">
62720
+ deviceId ou jwtToken n\xE3o dispon\xEDveis para carregar os atributos.
62721
+ </div>
62722
+ </div>`;
62723
+ return;
62724
+ }
62725
+ try {
62726
+ this.deviceProfileTab = new DeviceProfileTab({
62727
+ container,
62728
+ deviceId,
62729
+ jwtToken,
62730
+ tbBaseUrl: tbBaseUrl ?? window.location.origin
62731
+ });
62732
+ await this.deviceProfileTab.init();
62733
+ console.log("[SettingsModalView] DeviceProfileTab initialized");
62734
+ } catch (error) {
62735
+ console.error("[SettingsModalView] Failed to initialize DeviceProfileTab:", error);
62736
+ container.innerHTML = '<p style="color:#dc3545;padding:20px;text-align:center;">Erro ao carregar perfil do dispositivo</p>';
62737
+ }
62738
+ }
62187
62739
  // RFC-0104 / RFC-0180 / RFC-0198: Switch between tabs
62188
62740
  switchTab(tab) {
62189
62741
  this.currentTab = tab;
@@ -62196,11 +62748,13 @@ var SettingsModalView = class _SettingsModalView {
62196
62748
  const alarmsContent = this.modal.querySelector("#alarms-tab-content");
62197
62749
  const chamadosContent = this.modal.querySelector("#chamados-tab-content");
62198
62750
  const exclusionGroupsContent = this.modal.querySelector("#exclusion-groups-tab-content");
62751
+ const deviceProfileContent = this.modal.querySelector("#device-profile-tab-content");
62199
62752
  const incidentsContent = this.modal.querySelector("#incidents-tab-content");
62200
62753
  if (generalContent) generalContent.style.display = tab === "general" ? "block" : "none";
62201
62754
  if (annotationsContent) annotationsContent.style.display = tab === "annotations" ? "block" : "none";
62202
62755
  if (alarmsContent) alarmsContent.style.display = tab === "alarms" ? "block" : "none";
62203
62756
  if (chamadosContent) chamadosContent.style.display = tab === "chamados" ? "block" : "none";
62757
+ if (deviceProfileContent) deviceProfileContent.style.display = tab === "device-profile" ? "block" : "none";
62204
62758
  if (exclusionGroupsContent) exclusionGroupsContent.style.display = tab === "exclusion-groups" ? "block" : "none";
62205
62759
  if (incidentsContent) incidentsContent.style.display = tab === "incidents" ? "block" : "none";
62206
62760
  const saveBtn = this.modal.querySelector(".btn-save");
@@ -62227,6 +62781,10 @@ var SettingsModalView = class _SettingsModalView {
62227
62781
  this.exclusionGroupsTab.destroy();
62228
62782
  this.exclusionGroupsTab = null;
62229
62783
  }
62784
+ if (this.deviceProfileTab) {
62785
+ this.deviceProfileTab.destroy();
62786
+ this.deviceProfileTab = null;
62787
+ }
62230
62788
  if (this.incidentsDateRangeController) {
62231
62789
  this.incidentsDateRangeController.destroy();
62232
62790
  this.incidentsDateRangeController = null;
@@ -62397,6 +62955,15 @@ var SettingsModalView = class _SettingsModalView {
62397
62955
  Excluir Grupos
62398
62956
  <span class="modal-tab-badge modal-tab-badge--exclusion" id="tab-badge-exclusion-groups" style="display:none"></span>
62399
62957
  </button>
62958
+ <!-- Perfil de Dispositivo Tab \u2014 MyIO admins only (edits ThingsBoard directly) -->
62959
+ ${this.isSuperAdmin() ? `<button type="button" class="modal-tab" data-tab="device-profile">
62960
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
62961
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
62962
+ <line x1="3" y1="9" x2="21" y2="9"></line>
62963
+ <line x1="9" y1="21" x2="9" y2="9"></line>
62964
+ </svg>
62965
+ Perfil de Dispositivo
62966
+ </button>` : ""}
62400
62967
  <!-- RFC-0232: Incidentes (interpola\xE7\xE3o) \u2014 central + admin MyIO only -->
62401
62968
  ${this.config.isGateway && this.isSuperAdmin() ? `<button type="button" class="modal-tab" data-tab="incidents">
62402
62969
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -62444,6 +63011,13 @@ var SettingsModalView = class _SettingsModalView {
62444
63011
  <p>Carregando configura\xE7\xF5es de exclus\xE3o...</p>
62445
63012
  </div>
62446
63013
  </div>
63014
+ <!-- Perfil de Dispositivo Tab Content -->
63015
+ <div id="device-profile-tab-content" class="tab-content" style="display: none;">
63016
+ <div style="padding: 20px; text-align: center; color: #6c757d;">
63017
+ <div class="loading-spinner"></div>
63018
+ <p>Carregando perfil do dispositivo...</p>
63019
+ </div>
63020
+ </div>
62447
63021
  <!-- RFC-0232: Incidentes (interpola\xE7\xE3o) Tab Content \u2014 central + admin MyIO only.
62448
63022
  Rendered synchronously (data comes pre-fetched via config.interpolatedSlots,
62449
63023
  same as getGatewayInfoHTML() \u2014 no async loading state needed here). -->
@@ -162494,7 +163068,7 @@ var VirtualList = class {
162494
163068
 
162495
163069
  // src/components/header-annotations-panel/ExportModal.ts
162496
163070
  var MODAL_DOM_ID = "myio-annotations-export-modal";
162497
- var STYLES_ID3 = "myio-annotations-export-modal-styles";
163071
+ var STYLES_ID4 = "myio-annotations-export-modal-styles";
162498
163072
  var STYLES4 = `
162499
163073
  .${MODAL_DOM_ID}-backdrop {
162500
163074
  position: fixed; inset: 0; z-index: 99999;
@@ -162571,9 +163145,9 @@ var STYLES4 = `
162571
163145
  `;
162572
163146
  function injectStyles22() {
162573
163147
  if (typeof document === "undefined") return;
162574
- if (document.getElementById(STYLES_ID3)) return;
163148
+ if (document.getElementById(STYLES_ID4)) return;
162575
163149
  const el2 = document.createElement("style");
162576
- el2.id = STYLES_ID3;
163150
+ el2.id = STYLES_ID4;
162577
163151
  el2.textContent = STYLES4;
162578
163152
  document.head.appendChild(el2);
162579
163153
  }
@@ -165806,6 +166380,7 @@ var version = package_default.version || "0.0.0";
165806
166380
  DeviceOperationalCardGridController,
165807
166381
  DeviceOperationalCardGridView,
165808
166382
  DeviceOperationalCardView,
166383
+ DeviceProfileTab,
165809
166384
  DeviceStatusType,
165810
166385
  DomainType,
165811
166386
  ENERGY_SORT_OPTIONS,