myio-js-library 0.1.536 → 0.1.537

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
@@ -190,6 +190,9 @@ __export(src_exports, {
190
190
  FreshdeskClient: () => FreshdeskClient_exports,
191
191
  GRANULARITY_SELECTOR_CSS_PREFIX: () => GRANULARITY_SELECTOR_CSS_PREFIX,
192
192
  GRID_SORT_OPTIONS: () => GRID_SORT_OPTIONS,
193
+ GcdrAnnotationsClient: () => GcdrAnnotationsClient,
194
+ GcdrAnnotationsConflictError: () => GcdrAnnotationsConflictError,
195
+ GcdrAnnotationsError: () => GcdrAnnotationsError,
193
196
  GoalsModal: () => GoalsModal,
194
197
  GoalsMoneyApiError: () => GoalsMoneyApiError,
195
198
  GoalsMoneyClient: () => GoalsMoneyClient,
@@ -331,9 +334,13 @@ __export(src_exports, {
331
334
  WELCOME_DEFAULT_PALETTE: () => DEFAULT_PALETTE,
332
335
  Water: () => Water,
333
336
  WaterSummaryTooltip: () => WaterSummaryTooltip,
337
+ adaptGcdrListToLegacyAnnotations: () => adaptGcdrListToLegacyAnnotations,
338
+ adaptGcdrToLegacyAnnotation: () => adaptGcdrToLegacyAnnotation,
339
+ adaptLegacyToGcdrInput: () => adaptLegacyToGcdrInput,
334
340
  addAlarmBadge: () => addAlarmBadge,
335
341
  addDetectionContext: () => addDetectionContext,
336
342
  addFreshdeskTicketNote: () => addTicketNote,
343
+ addIncidentBadge: () => addIncidentBadge,
337
344
  addNamespace: () => addNamespace,
338
345
  addTicketBadge: () => addTicketBadge,
339
346
  aggregateByDay: () => aggregateByDay,
@@ -440,6 +447,8 @@ __export(src_exports, {
440
447
  createFilterModalComponent: () => createFilterModalComponent,
441
448
  createFooterComponent: () => createFooterComponent,
442
449
  createFreshdeskTicket: () => createTicket,
450
+ createGcdrAnnotationsAuthStrategy: () => createAuthStrategy,
451
+ createGcdrAnnotationsClient: () => createGcdrAnnotationsClient,
443
452
  createGoalsBarTooltip: () => createGoalsBarTooltip,
444
453
  createGoalsMoneyClient: () => createGoalsMoneyClient,
445
454
  createGranularitySelector: () => createGranularitySelector,
@@ -784,6 +793,7 @@ __export(src_exports, {
784
793
  recalculateDeviceStatus: () => recalculateDeviceStatus,
785
794
  recomputeDeviceGridPercentages: () => recomputePercentages,
786
795
  refreshAlarmBadges: () => refreshAlarmBadges,
796
+ refreshIncidentBadges: () => refreshIncidentBadges,
787
797
  refreshTicketBadges: () => refreshTicketBadges,
788
798
  removeAlarmsNotificationsPanelStyles: () => removeAlarmsNotificationsPanelStyles,
789
799
  removeBASDashboardStyles: () => removeBASDashboardStyles,
@@ -877,7 +887,7 @@ module.exports = __toCommonJS(src_exports);
877
887
  // package.json
878
888
  var package_default = {
879
889
  name: "myio-js-library",
880
- version: "0.1.536",
890
+ version: "0.1.537",
881
891
  description: "A clean, standalone JS SDK for MYIO projects",
882
892
  license: "MIT",
883
893
  repository: "github:gh-myio/myio-js-library",
@@ -2274,7 +2284,8 @@ var DeviceIconType = {
2274
2284
  TERMOSTATO: "TERMOSTATO",
2275
2285
  COMPRESSOR: "COMPRESSOR",
2276
2286
  VENTILADOR: "VENTILADOR",
2277
- SOLENOIDE: "SOLENOIDE"
2287
+ SOLENOIDE: "SOLENOIDE",
2288
+ GATEWAY: "GATEWAY"
2278
2289
  };
2279
2290
  var deviceIcons = {
2280
2291
  ESCADA_ROLANTE: "https://dashboard.myio-bas.com/api/images/public/EJ997iB2HD1AYYUHwIloyQOOszeqb2jp",
@@ -2299,7 +2310,10 @@ var deviceIcons = {
2299
2310
  COMPRESSOR: "https://dashboard.myio-bas.com/api/images/public/4BWMuVIFHnsfqatiV86DmTrOB7IF0X8Y",
2300
2311
  VENTILADOR: "https://dashboard.myio-bas.com/api/images/public/Rge8Q3t0CP5PW8XyTn9bBK9aVP6uzSTT",
2301
2312
  // SOLENOIDE: single representative (on); dynamic on/off/offline lives in solenoid-control SOLENOID_IMAGES.
2302
- SOLENOIDE: "https://dashboard.myio-bas.com/api/images/public/Tnq47Vd1TxhhqhYoHvzS73WVh1X84fPa"
2313
+ SOLENOIDE: "https://dashboard.myio-bas.com/api/images/public/Tnq47Vd1TxhhqhYoHvzS73WVh1X84fPa",
2314
+ // GATEWAY: central/gateway hardware (OrangePi) — used by SettingsModalView's
2315
+ // "Central" tab identity card. Previously fell through to DEFAULT_DEVICE_ICON.
2316
+ GATEWAY: "https://dashboard.myio-bas.com/api/images/public/kNlazDO8Yy90R5O12i17EqkQUFsNj44b"
2303
2317
  };
2304
2318
  var deviceIconLabels = {
2305
2319
  ESCADA_ROLANTE: "Escada Rolante",
@@ -2322,7 +2336,8 @@ var deviceIconLabels = {
2322
2336
  TERMOSTATO: "Termostato",
2323
2337
  COMPRESSOR: "Compressor",
2324
2338
  VENTILADOR: "Ventilador",
2325
- SOLENOIDE: "Solenoide"
2339
+ SOLENOIDE: "Solenoide",
2340
+ GATEWAY: "Gateway"
2326
2341
  };
2327
2342
  var DEFAULT_DEVICE_ICON = "https://dashboard.myio-bas.com/api/images/public/f9Ce4meybsdaAhAkUlAfy5ei3I4kcN4k";
2328
2343
  function getDeviceIcon(deviceProfile) {
@@ -5097,11 +5112,19 @@ var MODAL_HEADER_CSS = `
5097
5112
  }
5098
5113
 
5099
5114
  .myio-modal-header__title {
5100
- margin: 0;
5101
- font-size: 16px;
5102
- font-weight: 600;
5115
+ /* Rendered as an <h2> (see generateHTML() below) \u2014 on a real ThingsBoard
5116
+ dashboard (Angular Material) a global rule like ".mat-typography h2"
5117
+ has HIGHER specificity (class+element) than this single-class selector,
5118
+ regardless of load order, and wins with Material's own much larger
5119
+ font-size/line-height/margin \u2014 the header balloons in height there while
5120
+ looking fine in an isolated showcase page with no Material CSS at all.
5121
+ Same class of bug, same !important remedy already used in
5122
+ WelcomeModalView.ts's title rules. */
5123
+ margin: 0 !important;
5124
+ font-size: 16px !important;
5125
+ font-weight: 600 !important;
5103
5126
  color: white;
5104
- line-height: 1.4;
5127
+ line-height: 1.4 !important;
5105
5128
  white-space: nowrap;
5106
5129
  overflow: hidden;
5107
5130
  text-overflow: ellipsis;
@@ -8527,7 +8550,7 @@ function buildMyioIngestionAuth(config2) {
8527
8550
  const skewMs = renewSkewSeconds * 1e3;
8528
8551
  return now() >= cache.expiresAt - skewMs;
8529
8552
  }
8530
- async function sleep(ms) {
8553
+ async function sleep2(ms) {
8531
8554
  return new Promise((resolve) => setTimeout(resolve, ms));
8532
8555
  }
8533
8556
  async function requestNewToken() {
@@ -8571,7 +8594,7 @@ function buildMyioIngestionAuth(config2) {
8571
8594
  throw error;
8572
8595
  }
8573
8596
  const backoff = retryBaseMs * Math.pow(2, attempt - 1);
8574
- await sleep(backoff);
8597
+ await sleep2(backoff);
8575
8598
  }
8576
8599
  }
8577
8600
  }
@@ -9837,6 +9860,392 @@ async function createAnnotationIndicator(config2) {
9837
9860
  return indicator;
9838
9861
  }
9839
9862
 
9863
+ // src/components/gcdr-annotations/v1.0.0/authStrategies.ts
9864
+ var ApiKeyAuth = class {
9865
+ constructor(apiKey) {
9866
+ this.apiKey = apiKey;
9867
+ }
9868
+ apply(headers) {
9869
+ headers["X-API-Key"] = this.apiKey;
9870
+ }
9871
+ };
9872
+ var BearerAuth = class {
9873
+ constructor(token) {
9874
+ this.token = token;
9875
+ }
9876
+ async apply(headers) {
9877
+ const resolved = typeof this.token === "function" ? await this.token() : this.token;
9878
+ headers["Authorization"] = `Bearer ${resolved}`;
9879
+ }
9880
+ };
9881
+ function createAuthStrategy(auth) {
9882
+ const hasApiKey = !!auth.apiKey;
9883
+ const hasBearer = auth.bearerToken !== void 0 && auth.bearerToken !== null && auth.bearerToken !== "";
9884
+ if (hasApiKey && hasBearer) {
9885
+ throw new Error(
9886
+ "GcdrAnnotationsClient: provide exactly one of auth.apiKey or auth.bearerToken, not both."
9887
+ );
9888
+ }
9889
+ if (hasApiKey) return new ApiKeyAuth(auth.apiKey);
9890
+ if (hasBearer) return new BearerAuth(auth.bearerToken);
9891
+ throw new Error("GcdrAnnotationsClient: auth.apiKey or auth.bearerToken is required.");
9892
+ }
9893
+
9894
+ // src/components/gcdr-annotations/v1.0.0/adapters.ts
9895
+ var GCDR_TO_LEGACY_TYPE = {
9896
+ observation: "observation",
9897
+ pending: "pending",
9898
+ maintenance: "maintenance",
9899
+ activity: "activity"
9900
+ };
9901
+ var LEGACY_TO_GCDR_TYPE = {
9902
+ observation: "observation",
9903
+ pending: "pending",
9904
+ maintenance: "maintenance",
9905
+ activity: "activity"
9906
+ };
9907
+ var KNOWN_AUDIT_ACTIONS = /* @__PURE__ */ new Set([
9908
+ "created",
9909
+ "modified",
9910
+ "archived",
9911
+ "approved",
9912
+ "rejected",
9913
+ "commented",
9914
+ "acknowledged"
9915
+ ]);
9916
+ function toUserInfo(u) {
9917
+ return { id: u?.id ?? "", email: u?.email ?? "", name: u?.name ?? "" };
9918
+ }
9919
+ function deriveLegacyStatus(a) {
9920
+ if (a.finalized && a.finalizedReason === "archived") return "archived";
9921
+ return a.version > 1 ? "modified" : "created";
9922
+ }
9923
+ function adaptResponse(r, annotationId) {
9924
+ return {
9925
+ id: r.id,
9926
+ annotationId,
9927
+ type: r.type,
9928
+ text: r.text ?? "",
9929
+ createdAt: r.createdAt,
9930
+ createdBy: toUserInfo(r.createdBy)
9931
+ };
9932
+ }
9933
+ function adaptEvent(e) {
9934
+ const action = KNOWN_AUDIT_ACTIONS.has(e.action) ? e.action : "modified";
9935
+ return {
9936
+ timestamp: e.at,
9937
+ userId: e.by?.id ?? "",
9938
+ userName: e.by?.name ?? "",
9939
+ userEmail: e.by?.email ?? "",
9940
+ action
9941
+ };
9942
+ }
9943
+ function adaptGcdrToLegacyAnnotation(g) {
9944
+ const detail = g;
9945
+ const responses = (detail.responses ?? []).map((r) => adaptResponse(r, g.id));
9946
+ const history = (detail.events ?? []).map(adaptEvent);
9947
+ const approvedOrRejected = responses.find((r) => r.type === "approved" || r.type === "rejected");
9948
+ return {
9949
+ id: g.id,
9950
+ version: g.version,
9951
+ text: g.text,
9952
+ type: GCDR_TO_LEGACY_TYPE[g.type] ?? "observation",
9953
+ importance: g.importance,
9954
+ status: deriveLegacyStatus(g),
9955
+ createdAt: g.createdAt,
9956
+ dueDate: g.dueDate ?? void 0,
9957
+ createdBy: toUserInfo(g.createdBy),
9958
+ acknowledged: !!approvedOrRejected,
9959
+ acknowledgedBy: approvedOrRejected?.createdBy,
9960
+ acknowledgedAt: approvedOrRejected?.createdAt,
9961
+ responses,
9962
+ history
9963
+ };
9964
+ }
9965
+ function adaptGcdrListToLegacyAnnotations(list) {
9966
+ return list.map(adaptGcdrToLegacyAnnotation);
9967
+ }
9968
+ function adaptLegacyToGcdrInput(data, entity) {
9969
+ return {
9970
+ entityType: entity.entityType,
9971
+ entityId: entity.entityId,
9972
+ customerId: entity.customerId,
9973
+ text: data.text,
9974
+ type: LEGACY_TO_GCDR_TYPE[data.type] ?? "observation",
9975
+ importance: data.importance,
9976
+ dueDate: data.dueDate
9977
+ };
9978
+ }
9979
+
9980
+ // src/components/gcdr-annotations/v1.0.0/types.ts
9981
+ var GcdrAnnotationsError = class extends Error {
9982
+ status;
9983
+ code;
9984
+ constructor(status, message, code) {
9985
+ super(message || `HTTP_${status}`);
9986
+ this.name = "GcdrAnnotationsError";
9987
+ this.status = status;
9988
+ this.code = code;
9989
+ }
9990
+ };
9991
+ var GcdrAnnotationsConflictError = class extends GcdrAnnotationsError {
9992
+ finalizedOrStale = true;
9993
+ constructor(message) {
9994
+ super(409, message, "CONFLICT");
9995
+ this.name = "GcdrAnnotationsConflictError";
9996
+ }
9997
+ };
9998
+
9999
+ // src/components/gcdr-annotations/v1.0.0/GcdrAnnotationsClient.ts
10000
+ var DEFAULT_MAX_PAGES = 50;
10001
+ var DEFAULT_LIMIT = 100;
10002
+ var DEFAULT_MAX_RETRIES = 3;
10003
+ var DEFAULT_RETRY_BASE_MS = 500;
10004
+ function normalizeDomainPath(domainPath) {
10005
+ return String(domainPath).replace(/\/+$/, "");
10006
+ }
10007
+ function isNetworkError(err) {
10008
+ if (!err || typeof err !== "object") return false;
10009
+ const name = err.name ?? "";
10010
+ const message = String(err.message ?? "").toLowerCase();
10011
+ return name === "TypeError" || message.includes("network") || message.includes("failed to fetch");
10012
+ }
10013
+ function sleep(ms) {
10014
+ return new Promise((resolve) => setTimeout(resolve, ms));
10015
+ }
10016
+ var GcdrAnnotationsClient = class {
10017
+ domainPath;
10018
+ authStrategy;
10019
+ fetchImpl;
10020
+ logger;
10021
+ cacheTtlMs;
10022
+ maxPages;
10023
+ cache = /* @__PURE__ */ new Map();
10024
+ constructor(params) {
10025
+ if (!params || !params.domainPath) {
10026
+ throw new Error("GcdrAnnotationsClient requires a domainPath.");
10027
+ }
10028
+ this.domainPath = normalizeDomainPath(params.domainPath);
10029
+ this.authStrategy = createAuthStrategy(params.auth);
10030
+ const injected = params.fetchImpl || globalThis.fetch;
10031
+ if (typeof injected !== "function") {
10032
+ throw new Error("GcdrAnnotationsClient requires a fetch implementation.");
10033
+ }
10034
+ this.fetchImpl = params.fetchImpl ? params.fetchImpl : injected.bind(globalThis);
10035
+ this.logger = params.logger ?? console;
10036
+ this.cacheTtlMs = params.cacheTtlMs ?? 0;
10037
+ this.maxPages = params.maxPages ?? DEFAULT_MAX_PAGES;
10038
+ }
10039
+ /** Drops every cached GET response. Called automatically after any write. */
10040
+ invalidate() {
10041
+ this.cache.clear();
10042
+ }
10043
+ // ── Reads ──────────────────────────────────────────────────────────────
10044
+ /** One page of `GET /annotations` with the given filters. */
10045
+ async list(params = {}) {
10046
+ const query = new URLSearchParams();
10047
+ if (params.customerId) query.set("customerId", params.customerId);
10048
+ if (params.entityType) query.set("entityType", params.entityType);
10049
+ if (params.entityId) query.set("entityId", params.entityId);
10050
+ if (params.type) query.set("type", params.type);
10051
+ if (params.status) query.set("status", params.status);
10052
+ if (params.importance !== void 0) query.set("importance", String(params.importance));
10053
+ if (params.includeArchived !== void 0) query.set("includeArchived", String(params.includeArchived));
10054
+ query.set("page", String(params.page ?? 1));
10055
+ query.set("limit", String(params.limit ?? DEFAULT_LIMIT));
10056
+ const raw = await this._requestJson(`/annotations?${query.toString()}`, { method: "GET" });
10057
+ return this._toListPage(raw, params.page ?? 1, params.limit ?? DEFAULT_LIMIT);
10058
+ }
10059
+ /** Every annotation of a customer, aggregated across as many pages as it takes (capped at `maxPages`). */
10060
+ async listByCustomer(customerId, params = {}) {
10061
+ return this._listAll({ ...params, customerId });
10062
+ }
10063
+ /** Every annotation attached to one entity (e.g. a device, or a central identified by its GCDR UUID). */
10064
+ async listByEntity(entityType, entityId, params = {}) {
10065
+ return this._listAll({ ...params, entityType, entityId });
10066
+ }
10067
+ /** Full detail — responses, events (audit trail), mentions, attachments. */
10068
+ async getById(id) {
10069
+ const raw = await this._requestJson(`/annotations/${encodeURIComponent(id)}`, { method: "GET" });
10070
+ return this._unwrap(raw);
10071
+ }
10072
+ /** Convenience bridge — same `Annotation[]` shape `AnnotationsTab`/orchestrators already consume. */
10073
+ toLegacyAnnotations(list) {
10074
+ return adaptGcdrListToLegacyAnnotations(list);
10075
+ }
10076
+ // ── Writes ─────────────────────────────────────────────────────────────
10077
+ async create(input) {
10078
+ const raw = await this._requestJson("/annotations", {
10079
+ method: "POST",
10080
+ body: JSON.stringify(input)
10081
+ });
10082
+ this.invalidate();
10083
+ return this._unwrap(raw);
10084
+ }
10085
+ /** Optimistic lock via `If-Match: "<version>"` — a stale version raises `GcdrAnnotationsConflictError`. */
10086
+ async patch(id, changes, version2) {
10087
+ const raw = await this._requestJson(`/annotations/${encodeURIComponent(id)}`, {
10088
+ method: "PATCH",
10089
+ body: JSON.stringify(changes),
10090
+ headers: { "If-Match": `"${version2}"` }
10091
+ });
10092
+ this.invalidate();
10093
+ return this._unwrap(raw);
10094
+ }
10095
+ async archive(id, version2) {
10096
+ const raw = await this._requestJson(`/annotations/${encodeURIComponent(id)}/archive`, {
10097
+ method: "POST",
10098
+ headers: { "If-Match": `"${version2}"` }
10099
+ });
10100
+ this.invalidate();
10101
+ return this._unwrap(raw);
10102
+ }
10103
+ /**
10104
+ * Posts a response (comment/approve/reject/archive). `text` is required for
10105
+ * every type except `approved` — mirrored client-side so a caller gets an
10106
+ * immediate, local error instead of a round trip (API guide §5 rule).
10107
+ */
10108
+ async respond(id, input) {
10109
+ if (input.type !== "approved" && !input.text) {
10110
+ throw new GcdrAnnotationsError(400, `respond(): 'text' is required for response type '${input.type}'.`);
10111
+ }
10112
+ const raw = await this._requestJson(`/annotations/${encodeURIComponent(id)}/responses`, {
10113
+ method: "POST",
10114
+ body: JSON.stringify(input)
10115
+ });
10116
+ this.invalidate();
10117
+ return this._unwrap(raw);
10118
+ }
10119
+ async mention(id, userId) {
10120
+ await this._requestJson(`/annotations/${encodeURIComponent(id)}/mentions`, {
10121
+ method: "POST",
10122
+ body: JSON.stringify({ userId })
10123
+ });
10124
+ this.invalidate();
10125
+ }
10126
+ async attach(id, attachment) {
10127
+ await this._requestJson(`/annotations/${encodeURIComponent(id)}/attachments`, {
10128
+ method: "POST",
10129
+ body: JSON.stringify(attachment)
10130
+ });
10131
+ this.invalidate();
10132
+ }
10133
+ async detach(id, attachmentId) {
10134
+ await this._requestJson(
10135
+ `/annotations/${encodeURIComponent(id)}/attachments/${encodeURIComponent(attachmentId)}`,
10136
+ { method: "DELETE" }
10137
+ );
10138
+ this.invalidate();
10139
+ }
10140
+ // ── Internals ────────────────────────────────────────────────────────────
10141
+ async _listAll(params) {
10142
+ const limit = params.limit ?? DEFAULT_LIMIT;
10143
+ const out = [];
10144
+ for (let page = 1; page <= this.maxPages; page++) {
10145
+ const result = await this.list({ ...params, page, limit });
10146
+ out.push(...result.items);
10147
+ if (!result.pagination.hasNext) return out;
10148
+ if (page === this.maxPages) {
10149
+ this.logger.warn(
10150
+ `[GcdrAnnotationsClient] hit hard page cap of ${this.maxPages} \u2014 some annotations may be missing.`
10151
+ );
10152
+ }
10153
+ }
10154
+ return out;
10155
+ }
10156
+ _toListPage(raw, page, limit) {
10157
+ const body = raw;
10158
+ if (Array.isArray(body)) {
10159
+ return { items: body, pagination: { page, limit, hasNext: false } };
10160
+ }
10161
+ const items = Array.isArray(body?.data) ? body.data : [];
10162
+ const pagination = body?.pagination ?? {
10163
+ page,
10164
+ limit,
10165
+ hasNext: items.length >= limit
10166
+ };
10167
+ return { items, pagination };
10168
+ }
10169
+ /** Unwraps the `{success, data}` envelope; tolerates an already-flat body (test doubles, future API versions). */
10170
+ _unwrap(raw) {
10171
+ const envelope = raw;
10172
+ if (envelope && typeof envelope === "object" && "data" in envelope) {
10173
+ return envelope.data;
10174
+ }
10175
+ return raw;
10176
+ }
10177
+ async _requestJson(path, init) {
10178
+ const url = `${this.domainPath}${path.startsWith("/") ? path : `/${path}`}`;
10179
+ const method = init.method ?? "GET";
10180
+ const cacheKey = `${method}::${url}`;
10181
+ if (method === "GET" && this.cacheTtlMs > 0) {
10182
+ const cached = this.cache.get(cacheKey);
10183
+ if (cached && Date.now() - cached.at < this.cacheTtlMs) {
10184
+ return cached.value;
10185
+ }
10186
+ }
10187
+ const result = await this._doRequest(url, init);
10188
+ if (method === "GET" && this.cacheTtlMs > 0) {
10189
+ this.cache.set(cacheKey, { at: Date.now(), value: result });
10190
+ }
10191
+ return result;
10192
+ }
10193
+ async _doRequest(url, init) {
10194
+ let attempt = 0;
10195
+ while (true) {
10196
+ const headers = { Accept: "application/json", ...init.headers || {} };
10197
+ if (init.body !== void 0) headers["Content-Type"] = "application/json";
10198
+ await this.authStrategy.apply(headers);
10199
+ try {
10200
+ const res = await this.fetchImpl(url, { ...init, headers });
10201
+ if ((res.status === 429 || res.status >= 500) && attempt < DEFAULT_MAX_RETRIES) {
10202
+ const backoffMs = DEFAULT_RETRY_BASE_MS * Math.pow(2, attempt);
10203
+ this.logger.warn(
10204
+ `[GcdrAnnotationsClient] ${res.status} on ${url} \u2014 retry ${attempt + 1}/${DEFAULT_MAX_RETRIES} in ${backoffMs}ms`
10205
+ );
10206
+ attempt++;
10207
+ await sleep(backoffMs);
10208
+ continue;
10209
+ }
10210
+ const body = await this._readBody(res);
10211
+ if (res.status === 409) {
10212
+ const err = body && body.error;
10213
+ throw new GcdrAnnotationsConflictError(err?.message || res.statusText);
10214
+ }
10215
+ if (!res.ok) {
10216
+ const err = body && body.error;
10217
+ throw new GcdrAnnotationsError(res.status, err?.message || res.statusText, err?.code);
10218
+ }
10219
+ return body;
10220
+ } catch (err) {
10221
+ if (err instanceof GcdrAnnotationsError) throw err;
10222
+ if (isNetworkError(err) && attempt < DEFAULT_MAX_RETRIES) {
10223
+ const backoffMs = DEFAULT_RETRY_BASE_MS * Math.pow(2, attempt);
10224
+ this.logger.warn(
10225
+ `[GcdrAnnotationsClient] network error on ${url} \u2014 retry ${attempt + 1}/${DEFAULT_MAX_RETRIES} in ${backoffMs}ms`
10226
+ );
10227
+ attempt++;
10228
+ await sleep(backoffMs);
10229
+ continue;
10230
+ }
10231
+ throw new GcdrAnnotationsError(0, err instanceof Error ? err.message : "network error");
10232
+ }
10233
+ }
10234
+ }
10235
+ async _readBody(res) {
10236
+ try {
10237
+ const text2 = await res.text();
10238
+ if (!text2) return null;
10239
+ return JSON.parse(text2);
10240
+ } catch {
10241
+ return null;
10242
+ }
10243
+ }
10244
+ };
10245
+ function createGcdrAnnotationsClient(params) {
10246
+ return new GcdrAnnotationsClient(params);
10247
+ }
10248
+
9840
10249
  // src/utils/devices/deviceType.js
9841
10250
  function normalize(str) {
9842
10251
  if (typeof str !== "string") return "";
@@ -61047,9 +61456,273 @@ function createTicketsTab(config2) {
61047
61456
  return { destroy: () => tab.destroy() };
61048
61457
  }
61049
61458
 
61459
+ // src/components/createInputDateRangePickerInsideDIV.ts
61460
+ var PREMIUM_STYLES = `
61461
+ .myio-daterange-wrapper {
61462
+ font-family: 'Nunito', system-ui, sans-serif;
61463
+ background: #f9f9f9;
61464
+ padding: 20px;
61465
+ border-radius: 8px;
61466
+ margin-bottom: 20px;
61467
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
61468
+ transition: all 0.2s ease;
61469
+ }
61470
+
61471
+ .myio-daterange-wrapper:hover {
61472
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
61473
+ }
61474
+
61475
+ .myio-daterange-label {
61476
+ display: block;
61477
+ margin-bottom: 8px;
61478
+ font-weight: 500;
61479
+ color: #333;
61480
+ font-size: 14px;
61481
+ line-height: 1.4;
61482
+ }
61483
+
61484
+ .myio-daterange-input {
61485
+ width: 100%;
61486
+ max-width: 300px;
61487
+ padding: 8px 12px;
61488
+ border: 1px solid #ddd;
61489
+ border-radius: 4px;
61490
+ background: white;
61491
+ font-size: 14px;
61492
+ color: #333;
61493
+ cursor: pointer;
61494
+ transition: all 0.2s ease;
61495
+ font-family: inherit;
61496
+ line-height: 1.4;
61497
+ }
61498
+
61499
+ .myio-daterange-input:hover {
61500
+ border-color: #4A148C;
61501
+ box-shadow: 0 0 0 2px rgba(74, 20, 140, 0.1);
61502
+ }
61503
+
61504
+ .myio-daterange-input:focus {
61505
+ outline: none;
61506
+ border-color: #4A148C;
61507
+ box-shadow: 0 0 0 3px rgba(74, 20, 140, 0.2);
61508
+ }
61509
+
61510
+ .myio-daterange-input::placeholder {
61511
+ color: #999;
61512
+ opacity: 1;
61513
+ }
61514
+
61515
+ .myio-daterange-helper {
61516
+ font-size: 12px;
61517
+ color: #666;
61518
+ margin-top: 4px;
61519
+ line-height: 1.3;
61520
+ transition: color 0.2s ease;
61521
+ }
61522
+
61523
+ .myio-daterange-helper.success {
61524
+ color: #28a745;
61525
+ font-weight: 500;
61526
+ }
61527
+
61528
+ .myio-daterange-helper.error {
61529
+ color: #dc3545;
61530
+ font-weight: 500;
61531
+ }
61532
+
61533
+ /* Responsive design */
61534
+ @media (max-width: 768px) {
61535
+ .myio-daterange-wrapper {
61536
+ padding: 16px;
61537
+ margin-bottom: 16px;
61538
+ }
61539
+
61540
+ .myio-daterange-input {
61541
+ max-width: 100%;
61542
+ font-size: 16px; /* Prevents zoom on iOS */
61543
+ }
61544
+ }
61545
+
61546
+ /* High contrast mode support */
61547
+ @media (prefers-contrast: high) {
61548
+ .myio-daterange-wrapper {
61549
+ border: 2px solid #000;
61550
+ }
61551
+
61552
+ .myio-daterange-input {
61553
+ border: 2px solid #000;
61554
+ }
61555
+ }
61556
+
61557
+ /* Reduced motion support */
61558
+ @media (prefers-reduced-motion: reduce) {
61559
+ .myio-daterange-wrapper,
61560
+ .myio-daterange-input,
61561
+ .myio-daterange-helper {
61562
+ transition: none;
61563
+ }
61564
+ }
61565
+ `;
61566
+ function injectPremiumStyles() {
61567
+ const styleId = "myio-daterange-premium-styles";
61568
+ if (document.getElementById(styleId)) {
61569
+ return;
61570
+ }
61571
+ const styleEl = document.createElement("style");
61572
+ styleEl.id = styleId;
61573
+ styleEl.textContent = PREMIUM_STYLES;
61574
+ document.head.appendChild(styleEl);
61575
+ console.log("[MyIO] Premium date range styles injected");
61576
+ }
61577
+ function validateId(id, context) {
61578
+ if (!id || typeof id !== "string") {
61579
+ throw new Error(`[createInputDateRangePickerInsideDIV] ${context} must be a non-empty string`);
61580
+ }
61581
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(id)) {
61582
+ throw new Error(`[createInputDateRangePickerInsideDIV] ${context} '${id}' is not a valid HTML ID`);
61583
+ }
61584
+ }
61585
+ async function createInputDateRangePickerInsideDIV(params) {
61586
+ const {
61587
+ containerId,
61588
+ inputId,
61589
+ label = "Per\xEDodo de Datas",
61590
+ placeholder = "Clique para selecionar per\xEDodo",
61591
+ pickerOptions = {},
61592
+ classNames = {},
61593
+ injectStyles: injectStyles23 = true,
61594
+ showHelper = true
61595
+ } = params;
61596
+ validateId(containerId, "containerId");
61597
+ validateId(inputId, "inputId");
61598
+ const container = document.getElementById(containerId);
61599
+ if (!container) {
61600
+ throw new Error(`[createInputDateRangePickerInsideDIV] Container '#${containerId}' not found`);
61601
+ }
61602
+ if (injectStyles23) {
61603
+ injectPremiumStyles();
61604
+ }
61605
+ let inputEl = document.getElementById(inputId);
61606
+ if (inputEl && inputEl.tagName.toLowerCase() !== "input") {
61607
+ throw new Error(`[createInputDateRangePickerInsideDIV] Element '#${inputId}' exists but is not an input element`);
61608
+ }
61609
+ const wrapper = document.createElement("div");
61610
+ wrapper.className = classNames.wrapper || "myio-daterange-wrapper";
61611
+ wrapper.setAttribute("data-myio-component", "daterange-input");
61612
+ wrapper.setAttribute("data-version", "1.0.0");
61613
+ let labelEl = null;
61614
+ if (label) {
61615
+ labelEl = document.createElement("label");
61616
+ labelEl.className = classNames.label || "myio-daterange-label";
61617
+ labelEl.textContent = label;
61618
+ labelEl.setAttribute("for", inputId);
61619
+ wrapper.appendChild(labelEl);
61620
+ }
61621
+ if (!inputEl) {
61622
+ inputEl = document.createElement("input");
61623
+ inputEl.type = "text";
61624
+ inputEl.id = inputId;
61625
+ inputEl.name = inputId;
61626
+ }
61627
+ inputEl.className = classNames.input || "myio-daterange-input";
61628
+ inputEl.readOnly = true;
61629
+ inputEl.placeholder = placeholder;
61630
+ inputEl.autocomplete = "off";
61631
+ inputEl.setAttribute("aria-label", label || "Date range selector");
61632
+ if (showHelper) {
61633
+ inputEl.setAttribute("aria-describedby", `${inputId}-helper`);
61634
+ }
61635
+ let helperEl = null;
61636
+ if (showHelper) {
61637
+ helperEl = document.createElement("div");
61638
+ helperEl.id = `${inputId}-helper`;
61639
+ helperEl.className = classNames.helper || "myio-daterange-helper";
61640
+ helperEl.setAttribute("aria-live", "polite");
61641
+ helperEl.style.display = "flex";
61642
+ helperEl.style.alignItems = "center";
61643
+ }
61644
+ wrapper.appendChild(inputEl);
61645
+ if (helperEl) {
61646
+ wrapper.appendChild(helperEl);
61647
+ }
61648
+ const existingWrapper = container.querySelector('[data-myio-component="daterange-input"]');
61649
+ if (existingWrapper) {
61650
+ console.warn(`[createInputDateRangePickerInsideDIV] Replacing existing daterange input in container '#${containerId}'`);
61651
+ existingWrapper.remove();
61652
+ }
61653
+ container.appendChild(wrapper);
61654
+ const enhancedOptions = {
61655
+ maxRangeDays: 31,
61656
+ onApply: (result) => {
61657
+ if (helperEl) {
61658
+ const startDate = new Date(result.startISO);
61659
+ const endDate = new Date(result.endISO);
61660
+ const days = Math.ceil((endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24)) + 1;
61661
+ helperEl.textContent = `Per\xEDodo selecionado: ${days} dia${days !== 1 ? "s" : ""}`;
61662
+ helperEl.className = (classNames.helper || "myio-daterange-helper") + " success";
61663
+ setTimeout(() => {
61664
+ if (helperEl) {
61665
+ helperEl.className = classNames.helper || "myio-daterange-helper";
61666
+ }
61667
+ }, 3e3);
61668
+ }
61669
+ if (pickerOptions.onApply) {
61670
+ pickerOptions.onApply(result);
61671
+ }
61672
+ },
61673
+ ...pickerOptions
61674
+ };
61675
+ let picker;
61676
+ try {
61677
+ picker = await createDateRangePicker2(inputEl, enhancedOptions);
61678
+ console.log(`[createInputDateRangePickerInsideDIV] Successfully initialized for input '#${inputId}'`);
61679
+ } catch (error) {
61680
+ wrapper.remove();
61681
+ throw new Error(`[createInputDateRangePickerInsideDIV] Failed to initialize date picker: ${error.message}`);
61682
+ }
61683
+ const controller = {
61684
+ input: inputEl,
61685
+ container,
61686
+ wrapper,
61687
+ picker,
61688
+ getDisplayValue: () => inputEl.value,
61689
+ getDates: () => picker.getDates(),
61690
+ setDates: (startISO, endISO) => {
61691
+ try {
61692
+ picker.setDates(startISO, endISO);
61693
+ } catch (error) {
61694
+ console.error(`[createInputDateRangePickerInsideDIV] Error setting dates:`, error);
61695
+ throw error;
61696
+ }
61697
+ },
61698
+ setHelperText: (text2, type = "default") => {
61699
+ if (helperEl) {
61700
+ helperEl.textContent = text2;
61701
+ const baseClass = classNames.helper || "myio-daterange-helper";
61702
+ helperEl.className = type === "default" ? baseClass : `${baseClass} ${type}`;
61703
+ }
61704
+ },
61705
+ destroy: () => {
61706
+ try {
61707
+ picker.destroy();
61708
+ console.log(`[createInputDateRangePickerInsideDIV] Date picker destroyed for input '#${inputId}'`);
61709
+ } catch (error) {
61710
+ console.warn(`[createInputDateRangePickerInsideDIV] Error destroying picker:`, error);
61711
+ }
61712
+ try {
61713
+ wrapper.remove();
61714
+ console.log(`[createInputDateRangePickerInsideDIV] Wrapper removed for input '#${inputId}'`);
61715
+ } catch (error) {
61716
+ console.warn(`[createInputDateRangePickerInsideDIV] Error removing wrapper:`, error);
61717
+ }
61718
+ }
61719
+ };
61720
+ return controller;
61721
+ }
61722
+
61050
61723
  // src/components/premium-modals/settings/SettingsModalView.ts
61051
61724
  var ALLOWED_EMAIL_DOMAIN = "@myio.com.br";
61052
- var SettingsModalView = class {
61725
+ var SettingsModalView = class _SettingsModalView {
61053
61726
  container;
61054
61727
  modal;
61055
61728
  form;
@@ -61064,14 +61737,24 @@ var SettingsModalView = class {
61064
61737
  chamadosTabHandle = null;
61065
61738
  // Exclusão de Grupos tab
61066
61739
  exclusionGroupsTab = null;
61067
- // "gateway": read-only Central identity/telemetry tab, only rendered when
61068
- // config.isGateway (see getGatewayInfoHTML()) — a real device never has it.
61069
61740
  currentTab = "general";
61070
61741
  currentUser = null;
61071
61742
  permissions = null;
61072
61743
  // RFC-0190: Exclude Groups Totals
61073
61744
  excludeGroupsEnabled = false;
61074
61745
  excludedGroups = [];
61746
+ // RFC-0232: Incidentes tab filters (search/devices/period) — client-side
61747
+ // state, all derived from `config.interpolatedSlots` (the host's one
61748
+ // pre-fetch). `incidentsIdSuffix` is only needed because
61749
+ // createInputDateRangePickerInsideDIV() does a global `document.getElementById`
61750
+ // internally — every other Incidentes element is found via `this.modal.querySelector`,
61751
+ // which is safely scoped even with duplicate ids across simultaneous instances.
61752
+ incidentsIdSuffix = Math.random().toString(36).slice(2, 10);
61753
+ incidentsAllSlots = [];
61754
+ incidentsSelectedDevices = /* @__PURE__ */ new Set();
61755
+ incidentsSearch = "";
61756
+ incidentsDateRange = { startISO: null, endISO: null };
61757
+ incidentsDateRangeController = null;
61075
61758
  constructor(config2) {
61076
61759
  this.config = config2;
61077
61760
  this.createModal();
@@ -61132,6 +61815,10 @@ var SettingsModalView = class {
61132
61815
  this.initAlarmsTab();
61133
61816
  this.initChamadosTab();
61134
61817
  this.initExclusionGroupsTab();
61818
+ if (this.config.isGateway && this.isSuperAdmin()) {
61819
+ this.bindIncidentsTabEvents();
61820
+ this.initIncidentsDateRange();
61821
+ }
61135
61822
  this._updateTabBadges(initialData);
61136
61823
  }
61137
61824
  // RFC-0104: Initialize the Annotations Tab
@@ -61355,13 +62042,13 @@ var SettingsModalView = class {
61355
62042
  const alarmsContent = this.modal.querySelector("#alarms-tab-content");
61356
62043
  const chamadosContent = this.modal.querySelector("#chamados-tab-content");
61357
62044
  const exclusionGroupsContent = this.modal.querySelector("#exclusion-groups-tab-content");
61358
- const gatewayContent = this.modal.querySelector("#gateway-tab-content");
62045
+ const incidentsContent = this.modal.querySelector("#incidents-tab-content");
61359
62046
  if (generalContent) generalContent.style.display = tab === "general" ? "block" : "none";
61360
62047
  if (annotationsContent) annotationsContent.style.display = tab === "annotations" ? "block" : "none";
61361
62048
  if (alarmsContent) alarmsContent.style.display = tab === "alarms" ? "block" : "none";
61362
62049
  if (chamadosContent) chamadosContent.style.display = tab === "chamados" ? "block" : "none";
61363
62050
  if (exclusionGroupsContent) exclusionGroupsContent.style.display = tab === "exclusion-groups" ? "block" : "none";
61364
- if (gatewayContent) gatewayContent.style.display = tab === "gateway" ? "block" : "none";
62051
+ if (incidentsContent) incidentsContent.style.display = tab === "incidents" ? "block" : "none";
61365
62052
  const saveBtn = this.modal.querySelector(".btn-save");
61366
62053
  if (saveBtn) saveBtn.style.display = tab === "general" ? "inline-flex" : "none";
61367
62054
  if (tab === "annotations" && this.annotationsTab) {
@@ -61386,6 +62073,10 @@ var SettingsModalView = class {
61386
62073
  this.exclusionGroupsTab.destroy();
61387
62074
  this.exclusionGroupsTab = null;
61388
62075
  }
62076
+ if (this.incidentsDateRangeController) {
62077
+ this.incidentsDateRangeController.destroy();
62078
+ this.incidentsDateRangeController = null;
62079
+ }
61389
62080
  if (this.originalActiveElement && "focus" in this.originalActiveElement) {
61390
62081
  this.originalActiveElement.focus();
61391
62082
  }
@@ -61552,15 +62243,15 @@ var SettingsModalView = class {
61552
62243
  Excluir Grupos
61553
62244
  <span class="modal-tab-badge modal-tab-badge--exclusion" id="tab-badge-exclusion-groups" style="display:none"></span>
61554
62245
  </button>
61555
- <!-- Central/gateway identity+telemetry tab \u2014 only for CentralSettingsModal (isGateway) -->
61556
- ${this.config.isGateway ? `<button type="button" class="modal-tab" data-tab="gateway">
62246
+ <!-- RFC-0232: Incidentes (interpola\xE7\xE3o) \u2014 central + admin MyIO only -->
62247
+ ${this.config.isGateway && this.isSuperAdmin() ? `<button type="button" class="modal-tab" data-tab="incidents">
61557
62248
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
61558
- <rect x="2" y="4" width="20" height="6" rx="1"></rect>
61559
- <rect x="2" y="14" width="20" height="6" rx="1"></rect>
61560
- <line x1="6" y1="7" x2="6.01" y2="7"></line>
61561
- <line x1="6" y1="17" x2="6.01" y2="17"></line>
62249
+ <path d="M12 9v4"></path>
62250
+ <path d="M12 17h.01"></path>
62251
+ <path d="M10.29 3.86l-8.18 14.14A2 2 0 0 0 3.93 21h16.14a2 2 0 0 0 1.82-2.99L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
61562
62252
  </svg>
61563
- Central
62253
+ Incidentes
62254
+ ${(this.config.interpolatedSlots?.length ?? 0) > 0 ? `<span class="modal-tab-badge modal-tab-badge--incidents">${this.config.interpolatedSlots.length}</span>` : ""}
61564
62255
  </button>` : ""}
61565
62256
  </div>
61566
62257
  <div class="modal-body">
@@ -61599,8 +62290,10 @@ var SettingsModalView = class {
61599
62290
  <p>Carregando configura\xE7\xF5es de exclus\xE3o...</p>
61600
62291
  </div>
61601
62292
  </div>
61602
- <!-- Central/gateway identity+telemetry tab (read-only) -->
61603
- ${this.config.isGateway ? `<div id="gateway-tab-content" class="tab-content" style="display: none;">${this.getGatewayInfoHTML()}</div>` : ""}
62293
+ <!-- RFC-0232: Incidentes (interpola\xE7\xE3o) Tab Content \u2014 central + admin MyIO only.
62294
+ Rendered synchronously (data comes pre-fetched via config.interpolatedSlots,
62295
+ same as getGatewayInfoHTML() \u2014 no async loading state needed here). -->
62296
+ ${this.config.isGateway && this.isSuperAdmin() ? `<div id="incidents-tab-content" class="tab-content" style="display: none;">${this.getInterpolationIncidentsHTML()}</div>` : ""}
61604
62297
  </div>
61605
62298
  <div class="modal-footer">
61606
62299
  <button type="button" class="btn-cancel">Fechar</button>
@@ -61684,6 +62377,10 @@ var SettingsModalView = class {
61684
62377
  </div>
61685
62378
  </div>
61686
62379
 
62380
+ <!-- Central/gateway identity+telemetry \u2014 only for CentralSettingsModal (isGateway);
62381
+ rendered directly in Geral instead of its own tab, right after the identity card. -->
62382
+ ${this.config.isGateway ? this.getGatewayInfoHTML() : ""}
62383
+
61687
62384
  <!-- Bottom Row: Connection Info spanning full width -->
61688
62385
  ${this.getConnectionInfoHTML()}
61689
62386
 
@@ -61888,70 +62585,450 @@ var SettingsModalView = class {
61888
62585
  parts.push(`${m}min`);
61889
62586
  return parts.join(" ");
61890
62587
  }
62588
+ /** pt-BR absolute timestamp WITH seconds, ISO-string input (mirrors the
62589
+ * seconds-precision formatting `getConnectionInfoHTML()` uses for its
62590
+ * disconnect-interval row). */
62591
+ formatIsoWithSeconds(iso) {
62592
+ if (!iso) return "\u2014";
62593
+ const ms = Date.parse(iso);
62594
+ if (Number.isNaN(ms)) return "\u2014";
62595
+ return new Date(ms).toLocaleString("pt-BR", {
62596
+ day: "2-digit",
62597
+ month: "2-digit",
62598
+ year: "numeric",
62599
+ hour: "2-digit",
62600
+ minute: "2-digit",
62601
+ second: "2-digit"
62602
+ });
62603
+ }
62604
+ /** "(Xd:YYhs:YYmins atrás)"-style relative time, ISO-string input — mirrors
62605
+ * the device "Conectado desde" pattern in `getConnectionInfoHTML()`. */
62606
+ formatIsoRelativeDetailed(iso) {
62607
+ const ms = iso ? Date.parse(iso) : NaN;
62608
+ if (Number.isNaN(ms)) return "";
62609
+ const diffMinutes = Math.floor((Date.now() - ms) / 6e4);
62610
+ const diffHours = Math.floor(diffMinutes / 60);
62611
+ const diffDays = Math.floor(diffHours / 24);
62612
+ if (diffDays > 0) {
62613
+ return `(${diffDays}d:${String(diffHours % 24).padStart(2, "0")}hs:${String(diffMinutes % 60).padStart(2, "0")}mins atr\xE1s)`;
62614
+ }
62615
+ if (diffHours > 0) return `(${diffHours}hs:${String(diffMinutes % 60).padStart(2, "0")}mins atr\xE1s)`;
62616
+ if (diffMinutes > 0) return `(${diffMinutes}mins atr\xE1s)`;
62617
+ return "(agora)";
62618
+ }
62619
+ /** "(Xd/h/min atrás)"-style relative time, ISO-string input — mirrors the
62620
+ * device "Último check status" pattern in `getConnectionInfoHTML()`. */
62621
+ formatIsoRelativeSimple(iso) {
62622
+ const ms = iso ? Date.parse(iso) : NaN;
62623
+ if (Number.isNaN(ms)) return "";
62624
+ const diffMinutes = Math.floor((Date.now() - ms) / 6e4);
62625
+ const diffHours = Math.floor(diffMinutes / 60);
62626
+ const diffDays = Math.floor(diffHours / 24);
62627
+ if (diffDays > 0) return `(${diffDays}d atr\xE1s)`;
62628
+ if (diffHours > 0) return `(${diffHours}h atr\xE1s)`;
62629
+ if (diffMinutes > 0) return `(${diffMinutes}min atr\xE1s)`;
62630
+ return "(agora)";
62631
+ }
61891
62632
  /**
61892
62633
  * Central/gateway identity + telemetry — read-only, sourced 1:1 from
61893
- * GCDR's `GET /api/v1/centrals/:id`. Every value carries a stable
61894
- * `id="gwinfo-<key>"` so a host (CentralSettingsModal) can patch
61895
- * `.textContent` after an async fetch resolves — this method itself only
61896
- * runs once, at construction time (createModal() is never re-invoked),
61897
- * so it can't pick up data that arrives after the modal is already built.
61898
- */
62634
+ * GCDR's `GET /api/v1/centrals/:id`. Rendered inline in the Geral tab
62635
+ * (right after the identity card) rather than its own tab — a real device
62636
+ * never sets `isGateway`, so this is fully inert for the device settings
62637
+ * modal. Every value carries a stable `id="gwinfo-<key>"` so a host
62638
+ * (CentralSettingsModal) can patch `.textContent` after an async fetch
62639
+ * resolves — this method itself only runs once, at construction time
62640
+ * (createModal() is never re-invoked), so it can't pick up data that
62641
+ * arrives after the modal is already built.
62642
+ */
62643
+ /**
62644
+ * "Status" text/color mapping for `GatewayInfo.connectionStatus`
62645
+ * (ONLINE|OFFLINE|DEGRADED|MAINTENANCE) — same visual vocabulary as
62646
+ * `getConnectionInfoHTML()`'s device `statusMap` (ONLINE/OFFLINE stay
62647
+ * ALL-CAPS, DEGRADED reads "Atenção"). Exposed as a static so
62648
+ * `CentralSettingsModal.patchGatewayInfoDom()` can mirror it exactly when
62649
+ * an async `onFetchSettings` re-renders this card's values.
62650
+ */
62651
+ static GATEWAY_CONN_STATUS_MAP = {
62652
+ ONLINE: { text: "ONLINE", color: "#22c55e" },
62653
+ OFFLINE: { text: "OFFLINE", color: "#ef4444" },
62654
+ DEGRADED: { text: "Aten\xE7\xE3o", color: "#f59e0b" },
62655
+ MAINTENANCE: { text: "Manuten\xE7\xE3o", color: "#3b82f6" }
62656
+ };
61899
62657
  getGatewayInfoHTML() {
61900
62658
  const g = this.config.gatewayInfo || {};
61901
62659
  const bool = (v) => v == null ? "\u2014" : v ? "Sim" : "N\xE3o";
61902
62660
  const num = (v, suffix = "") => v == null ? "\u2014" : `${v}${suffix}`;
61903
62661
  const txt = (v) => v == null || v === "" ? "\u2014" : v;
61904
- const row = (label, key, value) => `<div class="identity-date-row">
61905
- <div class="identity-date-label">${label}</div>
61906
- <div class="identity-date-value" id="gwinfo-${key}">${value}</div>
62662
+ const infoRow = (label, key, valueHtml, fullWidth = false) => `<div class="info-row${fullWidth ? " full-width" : ""}">
62663
+ <span class="info-label">${label}</span>
62664
+ <span class="info-value" id="gwinfo-${key}">${valueHtml}</span>
61907
62665
  </div>`;
62666
+ const connStatusInfo = _SettingsModalView.GATEWAY_CONN_STATUS_MAP[(g.connectionStatus || "").toUpperCase()] || {
62667
+ text: txt(g.connectionStatus),
62668
+ color: "#6b7280"
62669
+ };
62670
+ const isOnline = (g.connectionStatus || "").toUpperCase() === "ONLINE";
62671
+ const connectedSinceAbs = isOnline ? this.formatIso(g.lastGatewaySuccessCheckAt) : "\u2014";
62672
+ const connectedSinceRel = isOnline ? this.formatIsoRelativeDetailed(g.lastGatewaySuccessCheckAt) : "";
62673
+ const lastCheckAbs = this.formatIso(g.lastGatewayCheckAt);
62674
+ const lastCheckRel = this.formatIsoRelativeSimple(g.lastGatewayCheckAt);
62675
+ const lc = g.lastConsumptionTelemetry;
62676
+ const consumptionText = lc ? `${lc.value.toLocaleString("pt-BR")}${lc.unit ? ` ${lc.unit}` : ""} - ${this.formatIsoWithSeconds(
62677
+ lc.timestamp
62678
+ )} ${this.formatIsoRelativeSimple(lc.timestamp)}`.trim() : "\u2014";
62679
+ const connectionInfoCard = `
62680
+ <div class="form-card info-card-wide">
62681
+ <h4 class="section-title">
62682
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="vertical-align: text-bottom; margin-right: 6px;">
62683
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
62684
+ <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
62685
+ </svg>
62686
+ Informa\xE7\xF5es de Conex\xE3o
62687
+ </h4>
62688
+ <div class="info-grid">
62689
+ ${infoRow("Central:", "connCentral", txt(this.config.deviceLabel))}
62690
+ ${infoRow(
62691
+ "Status:",
62692
+ "connectionStatus",
62693
+ `<span style="color: ${connStatusInfo.color}; font-weight: 600;">${connStatusInfo.text}</span>`
62694
+ )}
62695
+ ${infoRow(
62696
+ "Conectado desde:",
62697
+ "lastGatewaySuccessCheckAt",
62698
+ `${connectedSinceAbs}${connectedSinceRel ? ` <span class="time-since">${connectedSinceRel}</span>` : ""}`
62699
+ )}
62700
+ ${infoRow(
62701
+ "\xDAltimo check status:",
62702
+ "lastGatewayCheckAt",
62703
+ `${lastCheckAbs}${lastCheckRel ? ` <span class="time-since">${lastCheckRel}</span>` : ""}`
62704
+ )}
62705
+ ${infoRow("\xDAltimo intervalo desconectado:", "disconnectInterval", "\u2014", true)}
62706
+ ${infoRow("\xDAltima Telemetria de Consumo:", "lastConsumptionTelemetry", consumptionText, true)}
62707
+ ${infoRow("Monitoramento habilitado:", "monitoringEnabled", bool(g.monitoringEnabled))}
62708
+ ${infoRow("Lat\xEAncia:", "lastGatewayCheckLatencyMs", num(g.lastGatewayCheckLatencyMs, "ms"))}
62709
+ ${infoRow("Resultado do probe:", "probeResult", txt(g.probeResult))}
62710
+ </div>
62711
+ </div>
62712
+ `;
61908
62713
  return `
62714
+ ${connectionInfoCard}
61909
62715
  <div class="gateway-info-grid">
61910
- <div class="form-card gateway-info-card">
62716
+ <div class="form-card gateway-info-card info-card-wide">
61911
62717
  <h4 class="section-title">Identifica\xE7\xE3o</h4>
61912
- <div class="gateway-info-rows">
61913
- ${row("UUID", "uuid", txt(this.config.deviceId))}
61914
- ${row("Serial Number", "serialNumber", txt(g.serialNumber))}
61915
- ${row("Hardware ID", "hardwareId", txt(g.hardwareId))}
61916
- ${row("Tipo", "type", txt(g.type))}
61917
- ${row("Status (cadastro)", "status", txt(g.status))}
61918
- ${row("Firmware", "firmwareVersion", txt(g.firmwareVersion))}
61919
- ${row("Software", "softwareVersion", txt(g.softwareVersion))}
61920
- ${row("Frequ\xEAncia (canal)", "frequency", num(g.frequency))}
62718
+ <div class="info-grid">
62719
+ ${infoRow("UUID:", "uuid", txt(this.config.deviceId))}
62720
+ ${infoRow("Serial Number:", "serialNumber", txt(g.serialNumber))}
62721
+ ${infoRow("Hardware ID:", "hardwareId", txt(g.hardwareId))}
62722
+ ${infoRow("Tipo:", "type", txt(g.type))}
62723
+ ${infoRow("Status (cadastro):", "status", txt(g.status))}
62724
+ ${infoRow("Firmware:", "firmwareVersion", txt(g.firmwareVersion))}
62725
+ ${infoRow("Software:", "softwareVersion", txt(g.softwareVersion))}
62726
+ ${infoRow("Frequ\xEAncia (canal):", "frequency", num(g.frequency))}
61921
62727
  </div>
61922
62728
  </div>
61923
- <div class="form-card gateway-info-card">
61924
- <h4 class="section-title">Conectividade (telemetria)</h4>
61925
- <div class="gateway-info-rows">
61926
- ${row("Status de conex\xE3o", "connectionStatus", txt(g.connectionStatus))}
61927
- ${row("Monitoramento habilitado", "monitoringEnabled", bool(g.monitoringEnabled))}
61928
- ${row("\xDAltima tentativa", "lastGatewayCheckAt", this.formatIso(g.lastGatewayCheckAt))}
61929
- ${row("\xDAltimo sucesso", "lastGatewaySuccessCheckAt", this.formatIso(g.lastGatewaySuccessCheckAt))}
61930
- ${row("Lat\xEAncia", "lastGatewayCheckLatencyMs", num(g.lastGatewayCheckLatencyMs, "ms"))}
61931
- ${row("Resultado do probe", "probeResult", txt(g.probeResult))}
61932
- </div>
61933
- </div>
61934
- <div class="form-card gateway-info-card">
62729
+ <div class="form-card gateway-info-card info-card-wide">
61935
62730
  <h4 class="section-title">Estat\xEDsticas</h4>
61936
- <div class="gateway-info-rows">
61937
- ${row("Dispositivos conectados", "statsConnectedDevices", num(g.stats?.connectedDevices))}
61938
- ${row("Regras ativas", "statsActiveRules", num(g.stats?.activeRules))}
61939
- ${row("Eventos de sync pendentes", "statsPendingSyncEvents", num(g.stats?.pendingSyncEvents))}
61940
- ${row("Uptime", "statsUptimeSeconds", this.formatUptime(g.stats?.uptimeSeconds))}
61941
- ${row("\xDAltimo heartbeat", "statsLastHeartbeatAt", this.formatIso(g.stats?.lastHeartbeatAt))}
62731
+ <div class="info-grid">
62732
+ ${infoRow("Dispositivos conectados:", "statsConnectedDevices", num(g.stats?.connectedDevices))}
62733
+ ${infoRow("Regras ativas:", "statsActiveRules", num(g.stats?.activeRules))}
62734
+ ${infoRow("Eventos de sync pendentes:", "statsPendingSyncEvents", num(g.stats?.pendingSyncEvents))}
62735
+ ${infoRow("Uptime:", "statsUptimeSeconds", this.formatUptime(g.stats?.uptimeSeconds))}
62736
+ ${infoRow("\xDAltimo heartbeat:", "statsLastHeartbeatAt", this.formatIso(g.stats?.lastHeartbeatAt))}
61942
62737
  </div>
61943
62738
  </div>
61944
- <div class="form-card gateway-info-card">
62739
+ <div class="form-card gateway-info-card info-card-wide">
61945
62740
  <h4 class="section-title">Metadados</h4>
61946
- <div class="gateway-info-rows">
61947
- ${row("Criado em", "createdAt", this.formatIso(g.createdAt))}
61948
- ${row("Atualizado em", "updatedAt", this.formatIso(g.updatedAt))}
61949
- ${row("Vers\xE3o", "version", num(g.version))}
62741
+ <div class="info-grid">
62742
+ ${infoRow("Criado em:", "createdAt", this.formatIso(g.createdAt))}
62743
+ ${infoRow("Atualizado em:", "updatedAt", this.formatIso(g.updatedAt))}
62744
+ ${infoRow("Vers\xE3o:", "version", num(g.version))}
61950
62745
  </div>
61951
62746
  </div>
61952
62747
  </div>
61953
62748
  `;
61954
62749
  }
62750
+ escHtml(v) {
62751
+ return String(v ?? "").replace(
62752
+ /[&<>"']/g,
62753
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
62754
+ );
62755
+ }
62756
+ incidentsDeviceKey(s) {
62757
+ return s.deviceName || `slave ${s.slaveId}`;
62758
+ }
62759
+ incidentsFmtHour(iso) {
62760
+ return new Date(iso).toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo", hour: "2-digit", minute: "2-digit" });
62761
+ }
62762
+ incidentsFmtDay(iso) {
62763
+ return new Date(iso).toLocaleDateString("pt-BR", {
62764
+ timeZone: "America/Sao_Paulo",
62765
+ day: "2-digit",
62766
+ month: "2-digit",
62767
+ year: "numeric"
62768
+ });
62769
+ }
62770
+ incidentsFmtFull(iso) {
62771
+ return iso == null ? "\u2014" : new Date(iso).toLocaleString("pt-BR", {
62772
+ timeZone: "America/Sao_Paulo",
62773
+ day: "2-digit",
62774
+ month: "2-digit",
62775
+ hour: "2-digit",
62776
+ minute: "2-digit"
62777
+ });
62778
+ }
62779
+ /**
62780
+ * RFC-0232 — "Incidentes" tab (central + admin MyIO only): what the
62781
+ * No-Consumption Interpolation agent (`data-ingestion-prod`) fabricated for
62782
+ * THIS central. Inspired by that app's own ledger
62783
+ * (`InterpolationPanel.tsx`'s "Logs" tab, ~L450-548) but simplified for a
62784
+ * single-central context: that page groups by GATEWAY first (it shows every
62785
+ * central at once) — here there's only one central, so grouping starts one
62786
+ * level down, by DEVICE, then by DAY, each day showing its hour slots.
62787
+ * `config.interpolatedSlots` arrives pre-fetched from the host — nothing is
62788
+ * fetched in here, ever; the header controls (busca/dispositivos/período)
62789
+ * only filter that one array client-side. This method runs once, at
62790
+ * construction time, and builds the STATIC shell (filters header + KPI/body
62791
+ * mount points); `refreshIncidentsView()` re-renders the KPI/body pair on
62792
+ * every filter change via `this.modal.querySelector`.
62793
+ */
62794
+ getInterpolationIncidentsHTML() {
62795
+ this.incidentsAllSlots = this.config.interpolatedSlots ?? [];
62796
+ const deviceKeys = Array.from(new Set(this.incidentsAllSlots.map((s) => this.incidentsDeviceKey(s)))).sort(
62797
+ (a, b) => a.localeCompare(b, "pt-BR")
62798
+ );
62799
+ this.incidentsSelectedDevices = new Set(deviceKeys);
62800
+ if (this.incidentsAllSlots.length === 0) {
62801
+ return `<div class="incidents-empty">Nenhuma interpola\xE7\xE3o registrada para esta central no per\xEDodo consultado pelo host.</div>`;
62802
+ }
62803
+ const devicesPanel = deviceKeys.map(
62804
+ (key) => `
62805
+ <label class="incidents-multiselect__item">
62806
+ <input type="checkbox" checked value="${this.escHtml(key)}" data-role="incidents-device-checkbox">
62807
+ <span>${this.escHtml(key)}</span>
62808
+ </label>`
62809
+ ).join("");
62810
+ return `
62811
+ <div class="incidents-filters">
62812
+ <div class="incidents-filter incidents-filter--search">
62813
+ <label class="incidents-filter__label">Buscar dispositivo</label>
62814
+ <input type="text" class="incidents-search-input" placeholder="Nome do dispositivo\u2026" data-role="incidents-search-input">
62815
+ </div>
62816
+ <div class="incidents-filter incidents-filter--devices">
62817
+ <label class="incidents-filter__label">Dispositivos</label>
62818
+ <div class="incidents-multiselect">
62819
+ <button type="button" class="incidents-multiselect__toggle" data-role="incidents-devices-toggle">
62820
+ <span data-role="incidents-devices-summary">Todos (${deviceKeys.length})</span>
62821
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
62822
+ </button>
62823
+ <div class="incidents-multiselect__panel" data-role="incidents-devices-panel" hidden>
62824
+ <div class="incidents-multiselect__actions">
62825
+ <button type="button" data-role="incidents-devices-all">Marcar todos</button>
62826
+ <button type="button" data-role="incidents-devices-none">Desmarcar todos</button>
62827
+ </div>
62828
+ ${devicesPanel}
62829
+ </div>
62830
+ </div>
62831
+ </div>
62832
+ <div class="incidents-filter incidents-filter--period">
62833
+ <label class="incidents-filter__label">Per\xEDodo</label>
62834
+ <div id="incidents-daterange-${this.incidentsIdSuffix}" class="incidents-daterange-mount"></div>
62835
+ </div>
62836
+ </div>
62837
+ <div class="incidents-kpis">${this.buildIncidentsKpisHtml(this.incidentsAllSlots)}</div>
62838
+ <div class="incidents-body">${this.buildIncidentsBodyHtml(this.incidentsAllSlots)}</div>
62839
+ `;
62840
+ }
62841
+ /** 4 small KPI cards summarizing the currently-filtered slot set. */
62842
+ buildIncidentsKpisHtml(slots) {
62843
+ if (slots.length === 0) {
62844
+ return `<div class="incidents-kpi-empty">Nenhuma interpola\xE7\xE3o corresponde aos filtros atuais.</div>`;
62845
+ }
62846
+ const deviceCount = new Set(slots.map((s) => this.incidentsDeviceKey(s))).size;
62847
+ const dayCount = new Set(slots.map((s) => this.incidentsFmtDay(s.hourStart))).size;
62848
+ const withIncident = slots.filter((s) => s.incidentRef).length;
62849
+ const pct = slots.length ? Math.round(withIncident / slots.length * 100) : 0;
62850
+ const kpi = (label, value, sub) => `
62851
+ <div class="incidents-kpi">
62852
+ <div class="incidents-kpi__value">${value}</div>
62853
+ <div class="incidents-kpi__label">${label}</div>
62854
+ ${sub ? `<div class="incidents-kpi__sub">${sub}</div>` : ""}
62855
+ </div>`;
62856
+ return [
62857
+ kpi("Interpola\xE7\xF5es", String(slots.length)),
62858
+ kpi("Dispositivos", String(deviceCount)),
62859
+ kpi("Dias com atividade", String(dayCount)),
62860
+ kpi("Com incidente vinculado", String(withIncident), `${pct}% do total`)
62861
+ ].join("");
62862
+ }
62863
+ /** Device → day → hour-slot table, for whatever slot subset the caller already filtered. */
62864
+ buildIncidentsBodyHtml(slots) {
62865
+ if (slots.length === 0) {
62866
+ return `<div class="incidents-empty">Nenhuma interpola\xE7\xE3o corresponde aos filtros atuais.</div>`;
62867
+ }
62868
+ const byDevice = /* @__PURE__ */ new Map();
62869
+ for (const s of slots) {
62870
+ const key = this.incidentsDeviceKey(s);
62871
+ (byDevice.get(key) ?? byDevice.set(key, []).get(key)).push(s);
62872
+ }
62873
+ const deviceNames = Array.from(byDevice.keys()).sort((a, b) => a.localeCompare(b, "pt-BR"));
62874
+ const deviceSections = deviceNames.map((deviceName) => {
62875
+ const deviceSlots = byDevice.get(deviceName).slice().sort((a, b) => a.hourStart.localeCompare(b.hourStart));
62876
+ const byDay = /* @__PURE__ */ new Map();
62877
+ for (const s of deviceSlots) {
62878
+ const day = this.incidentsFmtDay(s.hourStart);
62879
+ (byDay.get(day) ?? byDay.set(day, []).get(day)).push(s);
62880
+ }
62881
+ const daySections = Array.from(byDay.entries()).map(([day, daySlots]) => {
62882
+ const rows = daySlots.map(
62883
+ (s) => `
62884
+ <tr>
62885
+ <td class="mono">${this.incidentsFmtHour(s.hourStart)}</td>
62886
+ <td class="num mono">${s.value.toFixed(1)}</td>
62887
+ <td class="mono muted">${this.incidentsFmtHour(s.sourceHour)}</td>
62888
+ <td class="mono muted" title="${this.escHtml(s.ruleId)}">${this.escHtml(s.ruleId.slice(0, 8))}</td>
62889
+ <td>${s.incidentRef ? `<code>${this.escHtml(s.incidentRef)}</code>` : '<span class="muted">\u2014</span>'}</td>
62890
+ <td class="muted">${this.incidentsFmtFull(s.createdAt)}</td>
62891
+ </tr>`
62892
+ ).join("");
62893
+ return `
62894
+ <div class="incidents-day">
62895
+ <div class="incidents-day__header">
62896
+ <span>${this.escHtml(day)}</span>
62897
+ <span class="incidents-day__count">${daySlots.length} hora${daySlots.length === 1 ? "" : "s"}</span>
62898
+ </div>
62899
+ <table class="incidents-table">
62900
+ <thead>
62901
+ <tr><th>Hora</th><th>Valor</th><th>Fonte LOCF</th><th>Regra</th><th>Incidente</th><th>Gravado</th></tr>
62902
+ </thead>
62903
+ <tbody>${rows}</tbody>
62904
+ </table>
62905
+ </div>`;
62906
+ }).join("");
62907
+ return `
62908
+ <div class="incidents-device">
62909
+ <button type="button" class="incidents-device__header" data-role="toggle-incidents-device">
62910
+ <svg class="incidents-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
62911
+ <polyline points="9 18 15 12 9 6"></polyline>
62912
+ </svg>
62913
+ <span class="incidents-device__name">${this.escHtml(deviceName)}</span>
62914
+ <span class="incidents-device__count">${deviceSlots.length} hora${deviceSlots.length === 1 ? "" : "s"} \xB7 ${byDay.size} dia${byDay.size === 1 ? "" : "s"}</span>
62915
+ </button>
62916
+ <div class="incidents-device__body">${daySections}</div>
62917
+ </div>`;
62918
+ }).join("");
62919
+ return `
62920
+ <div class="incidents-summary">${slots.length} interpola\xE7\xE3o(\xF5es) \xB7 ${deviceNames.length} dispositivo(s)</div>
62921
+ <div class="incidents-wrap">${deviceSections}</div>
62922
+ `;
62923
+ }
62924
+ /** Applies search + device + period filters to `incidentsAllSlots`. */
62925
+ getFilteredIncidentsSlots() {
62926
+ const search = this.incidentsSearch.trim().toLowerCase();
62927
+ const { startISO, endISO } = this.incidentsDateRange;
62928
+ return this.incidentsAllSlots.filter((s) => {
62929
+ const key = this.incidentsDeviceKey(s);
62930
+ if (!this.incidentsSelectedDevices.has(key)) return false;
62931
+ if (search && !key.toLowerCase().includes(search)) return false;
62932
+ if (startISO && s.hourStart < startISO) return false;
62933
+ if (endISO && s.hourStart > endISO) return false;
62934
+ return true;
62935
+ });
62936
+ }
62937
+ /** Re-renders the KPI cards + device/day body after any filter change (search/devices/period). */
62938
+ refreshIncidentsView() {
62939
+ const filtered = this.getFilteredIncidentsSlots();
62940
+ const kpisEl = this.modal.querySelector(".incidents-kpis");
62941
+ const bodyEl = this.modal.querySelector(".incidents-body");
62942
+ if (kpisEl) kpisEl.innerHTML = this.buildIncidentsKpisHtml(filtered);
62943
+ if (bodyEl) bodyEl.innerHTML = this.buildIncidentsBodyHtml(filtered);
62944
+ const summaryEl = this.modal.querySelector('[data-role="incidents-devices-summary"]');
62945
+ if (summaryEl) {
62946
+ const total = this.incidentsAllSlots.length ? new Set(this.incidentsAllSlots.map((s) => this.incidentsDeviceKey(s))).size : 0;
62947
+ const selected = this.incidentsSelectedDevices.size;
62948
+ summaryEl.textContent = selected === total ? `Todos (${total})` : `${selected} de ${total}`;
62949
+ }
62950
+ }
62951
+ /**
62952
+ * Wires the Incidentes tab's static controls (search input, devices
62953
+ * multiselect dropdown, device-group collapse). Called once from `render()`
62954
+ * — all delegated on `this.modal`, so it survives the KPI/body innerHTML
62955
+ * swaps `refreshIncidentsView()` does (those never touch the header). The
62956
+ * date-range picker is wired separately in `initIncidentsDateRange()`
62957
+ * (async — see that method).
62958
+ */
62959
+ bindIncidentsTabEvents() {
62960
+ this.modal.addEventListener("input", (event) => {
62961
+ const target = event.target;
62962
+ if (target.dataset.role !== "incidents-search-input") return;
62963
+ this.incidentsSearch = target.value;
62964
+ this.refreshIncidentsView();
62965
+ });
62966
+ this.modal.addEventListener("click", (event) => {
62967
+ const target = event.target;
62968
+ const deviceHeader = target.closest('[data-role="toggle-incidents-device"]');
62969
+ if (deviceHeader) {
62970
+ deviceHeader.closest(".incidents-device")?.classList.toggle("is-collapsed");
62971
+ return;
62972
+ }
62973
+ const panel = this.modal.querySelector('[data-role="incidents-devices-panel"]');
62974
+ const toggle = target.closest('[data-role="incidents-devices-toggle"]');
62975
+ if (toggle && panel) {
62976
+ panel.hidden = !panel.hidden;
62977
+ return;
62978
+ }
62979
+ if (target.dataset.role === "incidents-devices-all" || target.dataset.role === "incidents-devices-none") {
62980
+ const checkAll = target.dataset.role === "incidents-devices-all";
62981
+ this.modal.querySelectorAll('[data-role="incidents-device-checkbox"]').forEach((cb) => {
62982
+ cb.checked = checkAll;
62983
+ });
62984
+ this.incidentsSelectedDevices = checkAll ? new Set(this.incidentsAllSlots.map((s) => this.incidentsDeviceKey(s))) : /* @__PURE__ */ new Set();
62985
+ this.refreshIncidentsView();
62986
+ return;
62987
+ }
62988
+ if (panel && !panel.hidden && !target.closest('[data-role="incidents-devices-toggle"]') && !panel.contains(target)) {
62989
+ panel.hidden = true;
62990
+ }
62991
+ });
62992
+ this.modal.addEventListener("change", (event) => {
62993
+ const target = event.target;
62994
+ if (target.dataset.role !== "incidents-device-checkbox") return;
62995
+ const cb = target;
62996
+ if (cb.checked) this.incidentsSelectedDevices.add(cb.value);
62997
+ else this.incidentsSelectedDevices.delete(cb.value);
62998
+ this.refreshIncidentsView();
62999
+ });
63000
+ }
63001
+ /**
63002
+ * Mounts the period date-range picker into the Incidentes tab's header —
63003
+ * async (loads jQuery/moment/daterangepicker from CDN on first use, see
63004
+ * `createInputDateRangePickerInsideDIV`), so this runs from `render()`
63005
+ * AFTER the modal is attached to the DOM, same timing as
63006
+ * `initAnnotationsTab()`/`initAlarmsTab()`. No-ops when the tab isn't
63007
+ * rendered at all (non-admin / non-gateway).
63008
+ */
63009
+ async initIncidentsDateRange() {
63010
+ if (!(this.config.isGateway && this.isSuperAdmin())) return;
63011
+ const mountId = `incidents-daterange-${this.incidentsIdSuffix}`;
63012
+ if (!document.getElementById(mountId)) return;
63013
+ try {
63014
+ this.incidentsDateRangeController = await createInputDateRangePickerInsideDIV({
63015
+ containerId: mountId,
63016
+ inputId: `${mountId}-input`,
63017
+ label: "",
63018
+ placeholder: "Todo o per\xEDodo",
63019
+ showHelper: false,
63020
+ pickerOptions: {
63021
+ includeTime: true,
63022
+ onApply: (result) => {
63023
+ this.incidentsDateRange = { startISO: result.startISO, endISO: result.endISO };
63024
+ this.refreshIncidentsView();
63025
+ }
63026
+ }
63027
+ });
63028
+ } catch (err) {
63029
+ console.warn("[SettingsModalView] Incidentes: failed to mount date-range picker:", err);
63030
+ }
63031
+ }
61955
63032
  getDeviceImage(deviceType) {
61956
63033
  let normalized = (deviceType || "").toUpperCase();
61957
63034
  if (normalized === "3F_MEDIDOR") {
@@ -61960,8 +63037,7 @@ var SettingsModalView = class {
61960
63037
  normalized = profile.toUpperCase();
61961
63038
  }
61962
63039
  }
61963
- const DEFAULT = "https://cdn-icons-png.flaticon.com/512/1178/1178428.png";
61964
- const url = deviceIcons[normalized] || DEFAULT;
63040
+ const url = deviceIcons[normalized] || DEFAULT_DEVICE_ICON;
61965
63041
  return `<img src="${url}" class="identity-device-image" alt="${normalized}" />`;
61966
63042
  }
61967
63043
  getConsumptionLimits() {
@@ -62364,7 +63440,19 @@ var SettingsModalView = class {
62364
63440
  z-index: 9999999;
62365
63441
  font-family: 'Nunito', system-ui, sans-serif;
62366
63442
  }
62367
-
63443
+
63444
+ /* RFC-0232: the "Incidentes" period picker (createInputDateRangePickerInsideDIV
63445
+ -> jQuery daterangepicker) always appends its calendar dropdown to
63446
+ document.body, as a SIBLING of this overlay, not a descendant \u2014 so it
63447
+ can't be reached with a normal descendant selector. The plugin's own
63448
+ default z-index (~10000) is far below this overlay's 9999999 (chosen
63449
+ to beat ThingsBoard's Angular Material overlays in production), so
63450
+ without this override the calendar renders but is fully hidden behind
63451
+ the modal, making the input look like it "doesn't open". */
63452
+ .daterangepicker {
63453
+ z-index: 10000000 !important;
63454
+ }
63455
+
62368
63456
  .myio-device-settings-modal {
62369
63457
  background: white;
62370
63458
  border-radius: 8px;
@@ -62466,11 +63554,277 @@ var SettingsModalView = class {
62466
63554
  .modal-tab-badge--alarms { background: #dc2626; }
62467
63555
  .modal-tab-badge--chamados { background: #0891b2; }
62468
63556
  .modal-tab-badge--exclusion { background: #d97706; }
63557
+ .modal-tab-badge--incidents { background: #7C3AED; }
62469
63558
 
62470
63559
  .tab-content {
62471
63560
  min-height: 400px;
62472
63561
  }
62473
63562
 
63563
+ /* RFC-0232: Incidentes (interpola\xE7\xE3o) tab \u2014 device \u2192 day \u2192 hour slots. */
63564
+ .incidents-filters {
63565
+ display: flex;
63566
+ flex-wrap: wrap;
63567
+ align-items: flex-end;
63568
+ gap: 16px;
63569
+ margin-bottom: 16px;
63570
+ padding-bottom: 16px;
63571
+ border-bottom: 1px solid #e5e7eb;
63572
+ }
63573
+ .incidents-filter {
63574
+ display: flex;
63575
+ flex-direction: column;
63576
+ gap: 4px;
63577
+ }
63578
+ .incidents-filter--search { flex: 1 1 123px; min-width: 100px; }
63579
+ .incidents-filter--devices { flex: 0 0 auto; }
63580
+ .incidents-filter--period { flex: 0 0 auto; }
63581
+ .incidents-filter__label {
63582
+ font: 700 11px 'Nunito', system-ui, sans-serif;
63583
+ color: #6b7280;
63584
+ text-transform: uppercase;
63585
+ letter-spacing: 0.03em;
63586
+ }
63587
+ .incidents-search-input {
63588
+ width: 100%;
63589
+ padding: 7px 10px;
63590
+ border: 1px solid #d1d5db;
63591
+ border-radius: 6px;
63592
+ font: 600 13px 'Nunito', system-ui, sans-serif;
63593
+ color: #1f2937;
63594
+ box-sizing: border-box;
63595
+ }
63596
+ .incidents-search-input:focus { outline: 2px solid #7C3AED; outline-offset: 1px; }
63597
+ .incidents-multiselect { position: relative; }
63598
+ .incidents-multiselect__toggle {
63599
+ display: flex;
63600
+ align-items: center;
63601
+ gap: 8px;
63602
+ padding: 7px 12px;
63603
+ min-width: 200px;
63604
+ border: 1px solid #d1d5db;
63605
+ border-radius: 6px;
63606
+ background: #fff;
63607
+ font: 600 13px 'Nunito', system-ui, sans-serif;
63608
+ color: #1f2937;
63609
+ cursor: pointer;
63610
+ white-space: nowrap;
63611
+ }
63612
+ .incidents-multiselect__toggle:hover { border-color: #7C3AED; }
63613
+ .incidents-multiselect__panel {
63614
+ position: absolute;
63615
+ top: calc(100% + 4px);
63616
+ left: 0;
63617
+ z-index: 20;
63618
+ min-width: 220px;
63619
+ max-height: 260px;
63620
+ overflow-y: auto;
63621
+ background: #fff;
63622
+ border: 1px solid #d1d5db;
63623
+ border-radius: 8px;
63624
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
63625
+ padding: 8px;
63626
+ }
63627
+ .incidents-multiselect__actions {
63628
+ display: flex;
63629
+ gap: 8px;
63630
+ padding-bottom: 6px;
63631
+ margin-bottom: 6px;
63632
+ border-bottom: 1px solid #f3f4f6;
63633
+ }
63634
+ .incidents-multiselect__actions button {
63635
+ flex: 1;
63636
+ padding: 4px 6px;
63637
+ border: none;
63638
+ border-radius: 4px;
63639
+ background: #f5f3ff;
63640
+ color: #5b21b6;
63641
+ font: 700 11px 'Nunito', system-ui, sans-serif;
63642
+ cursor: pointer;
63643
+ }
63644
+ .incidents-multiselect__actions button:hover { background: #ede9fe; }
63645
+ .incidents-multiselect__item {
63646
+ display: flex;
63647
+ align-items: center;
63648
+ gap: 8px;
63649
+ padding: 5px 4px;
63650
+ font: 600 13px 'Nunito', system-ui, sans-serif;
63651
+ color: #374151;
63652
+ cursor: pointer;
63653
+ border-radius: 4px;
63654
+ }
63655
+ .incidents-multiselect__item:hover { background: #f9fafb; }
63656
+ .incidents-daterange-mount .myio-daterange-wrapper {
63657
+ margin-bottom: 0;
63658
+ padding: 0;
63659
+ background: transparent;
63660
+ box-shadow: none;
63661
+ }
63662
+ .incidents-daterange-mount {
63663
+ min-width: 280px;
63664
+ }
63665
+ .incidents-daterange-mount .myio-daterange-input {
63666
+ padding: 7px 10px;
63667
+ font-size: 13px;
63668
+ width: 100%;
63669
+ min-width: 280px;
63670
+ max-width: 340px;
63671
+ }
63672
+ .incidents-kpis {
63673
+ display: grid;
63674
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
63675
+ gap: 12px;
63676
+ margin-bottom: 16px;
63677
+ }
63678
+ .incidents-kpi {
63679
+ padding: 12px 14px;
63680
+ border-radius: 8px;
63681
+ background: #f5f3ff;
63682
+ border: 1px solid #e9d5ff;
63683
+ }
63684
+ .incidents-kpi__value {
63685
+ font: 800 22px 'Nunito', system-ui, sans-serif;
63686
+ color: #5b21b6;
63687
+ }
63688
+ .incidents-kpi__label {
63689
+ margin-top: 2px;
63690
+ font: 700 11px 'Nunito', system-ui, sans-serif;
63691
+ color: #6b7280;
63692
+ text-transform: uppercase;
63693
+ letter-spacing: 0.02em;
63694
+ }
63695
+ .incidents-kpi__sub {
63696
+ margin-top: 2px;
63697
+ font-size: 11px;
63698
+ color: #9ca3af;
63699
+ }
63700
+ .incidents-kpi-empty {
63701
+ margin-bottom: 16px;
63702
+ padding: 10px 14px;
63703
+ border-radius: 8px;
63704
+ background: #f9fafb;
63705
+ color: #9ca3af;
63706
+ font-size: 12px;
63707
+ }
63708
+ .theme-dark .incidents-filters { border-color: #374151; }
63709
+ .theme-dark .incidents-filter__label { color: #9ca3af; }
63710
+ .theme-dark .incidents-search-input { background: #111827; border-color: #4b5563; color: #e5e7eb; }
63711
+ .theme-dark .incidents-multiselect__toggle { background: #111827; border-color: #4b5563; color: #e5e7eb; }
63712
+ .theme-dark .incidents-multiselect__panel { background: #1f2937; border-color: #374151; }
63713
+ .theme-dark .incidents-multiselect__item { color: #e5e7eb; }
63714
+ .theme-dark .incidents-multiselect__item:hover { background: #111827; }
63715
+ .theme-dark .incidents-kpi { background: #2e1065; border-color: #4c1d95; }
63716
+ .theme-dark .incidents-kpi__value { color: #d8b4fe; }
63717
+ .theme-dark .incidents-kpi-empty { background: #111827; color: #6b7280; }
63718
+
63719
+ .incidents-summary {
63720
+ margin-bottom: 14px;
63721
+ font-size: 13px;
63722
+ font-weight: 600;
63723
+ color: #6b7280;
63724
+ }
63725
+ .incidents-empty {
63726
+ padding: 40px 20px;
63727
+ text-align: center;
63728
+ color: #6b7280;
63729
+ font-size: 14px;
63730
+ border: 1px dashed #d1d5db;
63731
+ border-radius: 8px;
63732
+ }
63733
+ .incidents-wrap {
63734
+ display: flex;
63735
+ flex-direction: column;
63736
+ gap: 12px;
63737
+ }
63738
+ .incidents-device {
63739
+ border: 1px solid #e5e7eb;
63740
+ border-radius: 8px;
63741
+ overflow: hidden;
63742
+ }
63743
+ .incidents-device__header {
63744
+ display: flex;
63745
+ align-items: center;
63746
+ gap: 8px;
63747
+ width: 100%;
63748
+ padding: 10px 14px;
63749
+ background: #f5f3ff;
63750
+ border: none;
63751
+ cursor: pointer;
63752
+ font: 700 13px 'Nunito', system-ui, sans-serif;
63753
+ color: #5b21b6;
63754
+ text-align: left;
63755
+ }
63756
+ .incidents-device__header:hover { background: #ede9fe; }
63757
+ /* Chevron SVG points right (\u25B6); rotated 90\xB0 to point down (\u25BC) while
63758
+ expanded \u2014 the default state, since a group starts open. */
63759
+ .incidents-chevron { flex-shrink: 0; transition: transform 0.15s ease; transform: rotate(90deg); }
63760
+ .incidents-device.is-collapsed .incidents-chevron { transform: rotate(0deg); }
63761
+ .incidents-device.is-collapsed .incidents-device__body { display: none; }
63762
+ .incidents-device__name { flex: 1; }
63763
+ .incidents-device__count {
63764
+ font-size: 11px;
63765
+ font-weight: 600;
63766
+ color: #7c3aed;
63767
+ background: #fff;
63768
+ border-radius: 999px;
63769
+ padding: 2px 8px;
63770
+ }
63771
+ .incidents-device__body {
63772
+ display: flex;
63773
+ flex-direction: column;
63774
+ gap: 10px;
63775
+ padding: 10px 14px;
63776
+ }
63777
+ .incidents-day__header {
63778
+ display: flex;
63779
+ align-items: center;
63780
+ justify-content: space-between;
63781
+ padding: 4px 2px;
63782
+ font: 700 12px 'Nunito', system-ui, sans-serif;
63783
+ color: #374151;
63784
+ }
63785
+ .incidents-day__count {
63786
+ font-size: 11px;
63787
+ font-weight: 600;
63788
+ color: #6b7280;
63789
+ }
63790
+ .incidents-table {
63791
+ width: 100%;
63792
+ border-collapse: collapse;
63793
+ font-size: 12px;
63794
+ }
63795
+ .incidents-table th {
63796
+ text-align: left;
63797
+ padding: 6px 8px;
63798
+ background: #f9fafb;
63799
+ color: #6b7280;
63800
+ font-weight: 600;
63801
+ border-bottom: 1px solid #e5e7eb;
63802
+ }
63803
+ .incidents-table td {
63804
+ padding: 6px 8px;
63805
+ border-bottom: 1px solid #f3f4f6;
63806
+ color: #1f2937;
63807
+ }
63808
+ .incidents-table td.num { text-align: right; }
63809
+ .incidents-table td.mono { font-family: 'Courier New', Courier, monospace; }
63810
+ .incidents-table td.muted { color: #9ca3af; }
63811
+ .incidents-table code {
63812
+ color: #7c3aed;
63813
+ background: #f5f3ff;
63814
+ border-radius: 4px;
63815
+ padding: 1px 5px;
63816
+ font-size: 11px;
63817
+ }
63818
+ .theme-dark .incidents-empty { border-color: #4b5563; color: #9ca3af; }
63819
+ .theme-dark .incidents-device { border-color: #374151; }
63820
+ .theme-dark .incidents-device__header { background: #2e1065; color: #d8b4fe; }
63821
+ .theme-dark .incidents-device__header:hover { background: #3b0764; }
63822
+ .theme-dark .incidents-device__count { background: #111827; color: #c4b5fd; }
63823
+ .theme-dark .incidents-day__header { color: #e5e7eb; }
63824
+ .theme-dark .incidents-table th { background: #111827; color: #9ca3af; border-color: #374151; }
63825
+ .theme-dark .incidents-table td { color: #e5e7eb; border-color: #1f2937; }
63826
+ .theme-dark .incidents-table code { background: #2e1065; }
63827
+
62474
63828
  /* Loading spinner for annotations tab */
62475
63829
  .loading-spinner {
62476
63830
  width: 32px;
@@ -62613,20 +63967,22 @@ var SettingsModalView = class {
62613
63967
  height: fit-content;
62614
63968
  }
62615
63969
 
62616
- /* "Central" tab (isGateway) \u2014 read-only identity/telemetry, 4 form-cards
62617
- in a responsive grid, each a stack of identity-date-row pairs. */
63970
+ /* Central/gateway identity+telemetry (isGateway), inline in Geral \u2014
63971
+ one section per line, same visual pattern as "Informa\xE7\xF5es de
63972
+ Conex\xE3o" (.info-card-wide + .info-grid/.info-row) for every card:
63973
+ Identifica\xE7\xE3o/Estat\xEDsticas/Metadados here, plus the Conectividade
63974
+ fieldset CentralSettingsModal injects after them.
63975
+ .gateway-info-grid is just a vertical stack \u2014 .gateway-info-card
63976
+ carries no sizing of its own any more (that lived here back when
63977
+ these were narrower, side-by-side flex-wrap cards); the full-width
63978
+ look now comes entirely from .info-card-wide. */
62618
63979
  .gateway-info-grid {
62619
- display: grid;
62620
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
62621
- gap: 16px;
62622
- }
62623
- .gateway-info-card .section-title {
62624
- margin-bottom: 12px;
62625
- }
62626
- .gateway-info-rows {
62627
63980
  display: flex;
62628
63981
  flex-direction: column;
62629
- gap: 10px;
63982
+ gap: 16px;
63983
+ }
63984
+ .gateway-info-card {
63985
+ margin: 0;
62630
63986
  }
62631
63987
 
62632
63988
  /* RFC-0180: Identity card \u2014 2-column \xD7 6-row grid */
@@ -64266,6 +65622,7 @@ function mapToGatewayInfo(data) {
64266
65622
  softwareVersion: data.softwareVersion ?? null,
64267
65623
  frequency: data.frequency ?? null,
64268
65624
  stats: data.stats ?? null,
65625
+ lastConsumptionTelemetry: data.lastConsumptionTelemetry ?? null,
64269
65626
  createdAt: data.createdAt ?? null,
64270
65627
  updatedAt: data.updatedAt ?? null,
64271
65628
  version: data.version ?? null
@@ -64283,6 +65640,43 @@ function formatIsoPtBr(iso) {
64283
65640
  minute: "2-digit"
64284
65641
  });
64285
65642
  }
65643
+ function formatIsoPtBrWithSeconds(iso) {
65644
+ if (!iso) return "\u2014";
65645
+ const ms = Date.parse(iso);
65646
+ if (Number.isNaN(ms)) return "\u2014";
65647
+ return new Date(ms).toLocaleString("pt-BR", {
65648
+ day: "2-digit",
65649
+ month: "2-digit",
65650
+ year: "numeric",
65651
+ hour: "2-digit",
65652
+ minute: "2-digit",
65653
+ second: "2-digit"
65654
+ });
65655
+ }
65656
+ function formatIsoRelativeDetailed(iso) {
65657
+ const ms = iso ? Date.parse(iso) : NaN;
65658
+ if (Number.isNaN(ms)) return "";
65659
+ const diffMinutes = Math.floor((Date.now() - ms) / 6e4);
65660
+ const diffHours = Math.floor(diffMinutes / 60);
65661
+ const diffDays = Math.floor(diffHours / 24);
65662
+ if (diffDays > 0) {
65663
+ return `(${diffDays}d:${String(diffHours % 24).padStart(2, "0")}hs:${String(diffMinutes % 60).padStart(2, "0")}mins atr\xE1s)`;
65664
+ }
65665
+ if (diffHours > 0) return `(${diffHours}hs:${String(diffMinutes % 60).padStart(2, "0")}mins atr\xE1s)`;
65666
+ if (diffMinutes > 0) return `(${diffMinutes}mins atr\xE1s)`;
65667
+ return "(agora)";
65668
+ }
65669
+ function formatIsoRelativeSimple(iso) {
65670
+ const ms = iso ? Date.parse(iso) : NaN;
65671
+ if (Number.isNaN(ms)) return "";
65672
+ const diffMinutes = Math.floor((Date.now() - ms) / 6e4);
65673
+ const diffHours = Math.floor(diffMinutes / 60);
65674
+ const diffDays = Math.floor(diffHours / 24);
65675
+ if (diffDays > 0) return `(${diffDays}d atr\xE1s)`;
65676
+ if (diffHours > 0) return `(${diffHours}h atr\xE1s)`;
65677
+ if (diffMinutes > 0) return `(${diffMinutes}min atr\xE1s)`;
65678
+ return "(agora)";
65679
+ }
64286
65680
  function formatUptime(seconds) {
64287
65681
  if (seconds == null || Number.isNaN(seconds)) return "\u2014";
64288
65682
  const d = Math.floor(seconds / 86400);
@@ -64299,6 +65693,10 @@ function patchGatewayInfoDom(root, uuid, data) {
64299
65693
  const el2 = root.querySelector(`#gwinfo-${key}`);
64300
65694
  if (el2) el2.textContent = value;
64301
65695
  };
65696
+ const setHtml = (key, html) => {
65697
+ const el2 = root.querySelector(`#gwinfo-${key}`);
65698
+ if (el2) el2.innerHTML = html;
65699
+ };
64302
65700
  set("uuid", uuid || "\u2014");
64303
65701
  set("serialNumber", data.serialNumber || "\u2014");
64304
65702
  set("hardwareId", data.hardwareId || "\u2014");
@@ -64307,10 +65705,33 @@ function patchGatewayInfoDom(root, uuid, data) {
64307
65705
  set("firmwareVersion", data.firmwareVersion || "\u2014");
64308
65706
  set("softwareVersion", data.softwareVersion || "\u2014");
64309
65707
  set("frequency", data.frequency != null ? String(data.frequency) : "\u2014");
64310
- set("connectionStatus", data.connectionStatus || "\u2014");
65708
+ set("connCentral", data.name || "\u2014");
65709
+ const connStatusInfo = SettingsModalView.GATEWAY_CONN_STATUS_MAP[(data.connectionStatus || "").toUpperCase()] || {
65710
+ text: data.connectionStatus || "\u2014",
65711
+ color: "#6b7280"
65712
+ };
65713
+ setHtml("connectionStatus", `<span style="color: ${connStatusInfo.color}; font-weight: 600;">${connStatusInfo.text}</span>`);
65714
+ const isOnline = (data.connectionStatus || "").toUpperCase() === "ONLINE";
65715
+ const connectedSinceAbs = isOnline ? formatIsoPtBr(data.lastGatewaySuccessCheckAt) : "\u2014";
65716
+ const connectedSinceRel = isOnline ? formatIsoRelativeDetailed(data.lastGatewaySuccessCheckAt) : "";
65717
+ setHtml(
65718
+ "lastGatewaySuccessCheckAt",
65719
+ `${connectedSinceAbs}${connectedSinceRel ? ` <span class="time-since">${connectedSinceRel}</span>` : ""}`
65720
+ );
65721
+ const lastCheckAbs = formatIsoPtBr(data.lastGatewayCheckAt);
65722
+ const lastCheckRel = formatIsoRelativeSimple(data.lastGatewayCheckAt);
65723
+ setHtml(
65724
+ "lastGatewayCheckAt",
65725
+ `${lastCheckAbs}${lastCheckRel ? ` <span class="time-since">${lastCheckRel}</span>` : ""}`
65726
+ );
65727
+ const lc = data.lastConsumptionTelemetry;
65728
+ setHtml(
65729
+ "lastConsumptionTelemetry",
65730
+ lc ? `${lc.value.toLocaleString("pt-BR")}${lc.unit ? ` ${lc.unit}` : ""} - ${formatIsoPtBrWithSeconds(
65731
+ lc.timestamp
65732
+ )} ${formatIsoRelativeSimple(lc.timestamp)}`.trim() : "\u2014"
65733
+ );
64311
65734
  set("monitoringEnabled", data.monitoringEnabled == null ? "\u2014" : data.monitoringEnabled ? "Sim" : "N\xE3o");
64312
- set("lastGatewayCheckAt", formatIsoPtBr(data.lastGatewayCheckAt));
64313
- set("lastGatewaySuccessCheckAt", formatIsoPtBr(data.lastGatewaySuccessCheckAt));
64314
65735
  set("lastGatewayCheckLatencyMs", data.lastGatewayCheckLatencyMs != null ? `${data.lastGatewayCheckLatencyMs}ms` : "\u2014");
64315
65736
  set("probeResult", data.probeResult || "\u2014");
64316
65737
  set("statsConnectedDevices", data.stats?.connectedDevices != null ? String(data.stats.connectedDevices) : "\u2014");
@@ -64325,33 +65746,38 @@ function patchGatewayInfoDom(root, uuid, data) {
64325
65746
  function connectivityFieldsetHtml(data, l, language, errors) {
64326
65747
  const err = (field) => errors[field] ? `<span class="myio-csettings__field-error">${escAttr4(errors[field])}</span>` : "";
64327
65748
  const errClass = (field) => errors[field] ? " myio-csettings__input--error" : "";
65749
+ const inputRow = (label, field, inputHtml) => `
65750
+ <div class="info-row">
65751
+ <span class="info-label">${escAttr4(label)}</span>
65752
+ <span class="info-value">${inputHtml}${err(field)}</span>
65753
+ </div>`;
64328
65754
  return `
64329
- <div class="form-card gateway-info-card myio-csettings-connectivity">
65755
+ <div class="form-card gateway-info-card info-card-wide myio-csettings-connectivity">
64330
65756
  <h4 class="section-title">${escAttr4(
64331
65757
  l.connectivitySection || (language === "en" ? "Connectivity Settings" : "Configura\xE7\xE3o de Conectividade")
64332
65758
  )}</h4>
64333
- <div class="myio-csettings__fields">
64334
- <label class="myio-csettings__field">
64335
- <span>${escAttr4(l.offlineGraceLabel || (language === "en" ? "Grace window (min)" : "Janela de gra\xE7a (min)"))}</span>
64336
- <input type="number" min="0" step="1" name="offlineGraceMinutes" value="${data.offlineGraceMinutes}" class="myio-csettings__input${errClass(
64337
- "offlineGraceMinutes"
64338
- )}" />
64339
- ${err("offlineGraceMinutes")}
64340
- </label>
64341
- <label class="myio-csettings__field">
64342
- <span>${escAttr4(l.blipToleranceLabel || (language === "en" ? "Blip tolerance (min)" : "Toler\xE2ncia de blip (min)"))}</span>
64343
- <input type="number" min="0" step="1" name="blipToleranceMinutes" value="${data.blipToleranceMinutes}" class="myio-csettings__input${errClass(
64344
- "blipToleranceMinutes"
64345
- )}" />
64346
- ${err("blipToleranceMinutes")}
64347
- </label>
64348
- <label class="myio-csettings__field">
64349
- <span>${escAttr4(l.offlineHardLabel || (language === "en" ? "Hard-offline threshold (min)" : "Limiar de hard-offline (min)"))}</span>
64350
- <input type="number" min="0" step="1" name="offlineHardMinutes" value="${data.offlineHardMinutes ?? ""}" placeholder="${escAttr4(l.offlineHardHint || (language === "en" ? "empty = disabled" : "vazio = desativado"))}" class="myio-csettings__input${errClass(
64351
- "offlineHardMinutes"
64352
- )}" />
64353
- ${err("offlineHardMinutes")}
64354
- </label>
65759
+ <div class="info-grid">
65760
+ ${inputRow(
65761
+ `${l.offlineGraceLabel || (language === "en" ? "Grace window (min)" : "Janela de gra\xE7a (min)")}:`,
65762
+ "offlineGraceMinutes",
65763
+ `<input type="number" min="0" step="1" name="offlineGraceMinutes" value="${data.offlineGraceMinutes}" class="myio-csettings__input${errClass(
65764
+ "offlineGraceMinutes"
65765
+ )}" />`
65766
+ )}
65767
+ ${inputRow(
65768
+ `${l.blipToleranceLabel || (language === "en" ? "Blip tolerance (min)" : "Toler\xE2ncia de blip (min)")}:`,
65769
+ "blipToleranceMinutes",
65770
+ `<input type="number" min="0" step="1" name="blipToleranceMinutes" value="${data.blipToleranceMinutes}" class="myio-csettings__input${errClass(
65771
+ "blipToleranceMinutes"
65772
+ )}" />`
65773
+ )}
65774
+ ${inputRow(
65775
+ `${l.offlineHardLabel || (language === "en" ? "Hard-offline threshold (min)" : "Limiar de hard-offline (min)")}:`,
65776
+ "offlineHardMinutes",
65777
+ `<input type="number" min="0" step="1" name="offlineHardMinutes" value="${data.offlineHardMinutes ?? ""}" placeholder="${escAttr4(l.offlineHardHint || (language === "en" ? "empty = disabled" : "vazio = desativado"))}" class="myio-csettings__input${errClass(
65778
+ "offlineHardMinutes"
65779
+ )}" />`
65780
+ )}
64355
65781
  </div>
64356
65782
  </div>
64357
65783
  `;
@@ -64362,16 +65788,13 @@ function injectStyles9() {
64362
65788
  const tag = document.createElement("style");
64363
65789
  tag.id = STYLE_ID4;
64364
65790
  tag.textContent = `
64365
- .myio-csettings__fields{display:flex;flex-direction:column;gap:10px;}
64366
- .myio-csettings__field{display:flex;flex-direction:column;gap:4px;}
64367
- .myio-csettings__field span{font:700 12px 'Nunito', system-ui, sans-serif;color:#374151;}
64368
65791
  .myio-csettings__input{
64369
- font:600 13px 'Nunito', system-ui, sans-serif;padding:7px 10px;border-radius:8px;
64370
- border:1px solid #e5e7eb;background:#fff;color:#1f2937;width:100%;box-sizing:border-box;
65792
+ font:600 13px 'Nunito', system-ui, sans-serif;padding:6px 8px;border-radius:6px;
65793
+ border:1px solid #e5e7eb;background:#fff;color:#1f2937;width:100px;box-sizing:border-box;
65794
+ text-align:right;
64371
65795
  }
64372
65796
  .myio-csettings__input--error{border-color:#dc2626;}
64373
- .myio-csettings__field-error{font:700 11px 'Nunito', system-ui, sans-serif;color:#dc2626;}
64374
- .theme-dark .myio-csettings__field span{color:#d1d5db;}
65797
+ .myio-csettings__field-error{display:block;margin-top:4px;font:700 11px 'Nunito', system-ui, sans-serif;color:#dc2626;text-align:right;}
64375
65798
  .theme-dark .myio-csettings__input{background:#111827;border-color:#4b5563;color:#e5e7eb;}
64376
65799
  `;
64377
65800
  document.head.appendChild(tag);
@@ -64403,6 +65826,12 @@ var CentralSettingsModal = class extends SettingsModalView {
64403
65826
  deviceId: params.id,
64404
65827
  deviceLabel: params.name,
64405
65828
  deviceName: params.uuid ?? void 0,
65829
+ // RFC-0200: identity-card image (Geral tab) — without this, getDeviceImage()
65830
+ // sees an empty deviceType and falls back to DEFAULT_DEVICE_ICON (generic
65831
+ // 3F_MEDIDOR art) instead of deviceIcons.GATEWAY. A real device never sets
65832
+ // deviceType to this value (it's not a valid TB deviceProfile), so this is
65833
+ // fully inert for the device settings modal, same as isGateway above.
65834
+ deviceType: "GATEWAY",
64406
65835
  // No domain concept for a central — suppresses the energy/water/
64407
65836
  // temperature-only sections and removes the "Excluir Grupos" tab
64408
65837
  // (gated on `domain === 'energy'` in the base class).
@@ -64412,11 +65841,15 @@ var CentralSettingsModal = class extends SettingsModalView {
64412
65841
  jwtToken: "",
64413
65842
  mapInstantaneousPower: {},
64414
65843
  deviceMapInstaneousPower: {},
64415
- // Native "Central" tab (see file doc) — read-only GCDR identity/
64416
- // telemetry. Real devices never set isGateway, so this is fully inert
65844
+ // Inline gateway identity/telemetry in Geral (see file doc) — read-only
65845
+ // GCDR data. Real devices never set isGateway, so this is fully inert
64417
65846
  // for the device settings modal.
64418
65847
  isGateway: true,
64419
65848
  gatewayInfo: mapToGatewayInfo(initialData),
65849
+ // RFC-0232: "Incidentes" tab gate + data — see CentralSettingsModalParams doc.
65850
+ userEmail: params.userEmail,
65851
+ superadmin: params.superadmin,
65852
+ interpolatedSlots: params.interpolatedSlots,
64420
65853
  onSave: () => Promise.resolve(),
64421
65854
  // rebound below once `this` exists (see class doc)
64422
65855
  onClose: () => {
@@ -64436,7 +65869,7 @@ var CentralSettingsModal = class extends SettingsModalView {
64436
65869
  }
64437
65870
  /** Overrides the base's public `render()`: mounts the full device-modal
64438
65871
  * shell via `super.render()`, then injects the Conectividade fieldset the
64439
- * base has no concept of, and patches the native "Central" tab's
65872
+ * base has no concept of, and patches the Geral tab's inline gateway-info
64440
65873
  * `#gwinfo-*` elements — its HTML was generated once at construction time
64441
65874
  * (from whatever data was available then), so an async `onFetchSettings`
64442
65875
  * resolving later needs this explicit patch to actually show up. */
@@ -64455,17 +65888,15 @@ var CentralSettingsModal = class extends SettingsModalView {
64455
65888
  }
64456
65889
  }
64457
65890
  /**
64458
- * Injects the editable connectivity fieldset into the native "Central"
64459
- * tab's `.gateway-info-grid` — as a 5th `.form-card`, right after the
64460
- * read-only "Conectividade (telemetria)" card (found by its heading text,
64461
- * not a fixed position, so this stays correct even if the base ever
64462
- * reorders `getGatewayInfoHTML()`'s 4 cards). Lives outside `<form>`
64463
- * (the Central tab isn't form-wrapped, unlike Geral) — `getFormData()` is
64464
- * overridden below to read these fields manually since `new
64465
- * FormData(this.form)` can't see them.
65891
+ * Injects the editable connectivity fieldset into the Geral tab's inline
65892
+ * gateway-info `.gateway-info-grid` (Identificação/Estatísticas/Metadados)
65893
+ * as a 4th `.form-card`, appended at the end. This grid is a descendant of
65894
+ * the Geral tab's own `<form>`, so the base's `new FormData(this.form)`
65895
+ * (used by the base's own `getFormData()`) picks up these inputs
65896
+ * automatically — no `getFormData()` override needed here.
64466
65897
  */
64467
65898
  injectConnectivitySection(errors) {
64468
- const grid = this.csRootEl?.querySelector("#gateway-tab-content .gateway-info-grid");
65899
+ const grid = this.csRootEl?.querySelector(".gateway-info-grid");
64469
65900
  if (!grid) return;
64470
65901
  const html = connectivityFieldsetHtml(this.csCurrentData, this.csLabels, this.csLanguage, errors);
64471
65902
  const existing = grid.querySelector(".myio-csettings-connectivity");
@@ -64473,30 +65904,7 @@ var CentralSettingsModal = class extends SettingsModalView {
64473
65904
  existing.outerHTML = html;
64474
65905
  return;
64475
65906
  }
64476
- const telemetryCard = Array.from(grid.querySelectorAll(".gateway-info-card")).find(
64477
- (card) => card.querySelector(".section-title")?.textContent === "Conectividade (telemetria)"
64478
- );
64479
- if (telemetryCard) {
64480
- telemetryCard.insertAdjacentHTML("afterend", html);
64481
- } else {
64482
- grid.insertAdjacentHTML("beforeend", html);
64483
- }
64484
- }
64485
- /**
64486
- * Overrides the base's public `getFormData()` (called internally by the
64487
- * base's own submit/save-button handlers via `this.getFormData()`, which
64488
- * dispatches here through normal prototype resolution — no base edit
64489
- * needed). Merges the base's `new FormData(this.form)` read (Geral tab
64490
- * fields) with a manual read of the connectivity inputs, which live in the
64491
- * Central tab, outside `this.form`.
64492
- */
64493
- getFormData() {
64494
- const base = super.getFormData();
64495
- const extra = {};
64496
- this.csRootEl?.querySelectorAll(".myio-csettings-connectivity [name]").forEach((el2) => {
64497
- if (el2.name) extra[el2.name] = el2.value;
64498
- });
64499
- return { ...base, ...extra };
65907
+ grid.insertAdjacentHTML("beforeend", html);
64500
65908
  }
64501
65909
  async handleCentralSave(formData) {
64502
65910
  const hardRaw = formData.offlineHardMinutes;
@@ -74322,270 +75730,6 @@ async function openDashboardPopupSettings(params) {
74322
75730
  }
74323
75731
  }
74324
75732
 
74325
- // src/components/createInputDateRangePickerInsideDIV.ts
74326
- var PREMIUM_STYLES = `
74327
- .myio-daterange-wrapper {
74328
- font-family: 'Nunito', system-ui, sans-serif;
74329
- background: #f9f9f9;
74330
- padding: 20px;
74331
- border-radius: 8px;
74332
- margin-bottom: 20px;
74333
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
74334
- transition: all 0.2s ease;
74335
- }
74336
-
74337
- .myio-daterange-wrapper:hover {
74338
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
74339
- }
74340
-
74341
- .myio-daterange-label {
74342
- display: block;
74343
- margin-bottom: 8px;
74344
- font-weight: 500;
74345
- color: #333;
74346
- font-size: 14px;
74347
- line-height: 1.4;
74348
- }
74349
-
74350
- .myio-daterange-input {
74351
- width: 100%;
74352
- max-width: 300px;
74353
- padding: 8px 12px;
74354
- border: 1px solid #ddd;
74355
- border-radius: 4px;
74356
- background: white;
74357
- font-size: 14px;
74358
- color: #333;
74359
- cursor: pointer;
74360
- transition: all 0.2s ease;
74361
- font-family: inherit;
74362
- line-height: 1.4;
74363
- }
74364
-
74365
- .myio-daterange-input:hover {
74366
- border-color: #4A148C;
74367
- box-shadow: 0 0 0 2px rgba(74, 20, 140, 0.1);
74368
- }
74369
-
74370
- .myio-daterange-input:focus {
74371
- outline: none;
74372
- border-color: #4A148C;
74373
- box-shadow: 0 0 0 3px rgba(74, 20, 140, 0.2);
74374
- }
74375
-
74376
- .myio-daterange-input::placeholder {
74377
- color: #999;
74378
- opacity: 1;
74379
- }
74380
-
74381
- .myio-daterange-helper {
74382
- font-size: 12px;
74383
- color: #666;
74384
- margin-top: 4px;
74385
- line-height: 1.3;
74386
- transition: color 0.2s ease;
74387
- }
74388
-
74389
- .myio-daterange-helper.success {
74390
- color: #28a745;
74391
- font-weight: 500;
74392
- }
74393
-
74394
- .myio-daterange-helper.error {
74395
- color: #dc3545;
74396
- font-weight: 500;
74397
- }
74398
-
74399
- /* Responsive design */
74400
- @media (max-width: 768px) {
74401
- .myio-daterange-wrapper {
74402
- padding: 16px;
74403
- margin-bottom: 16px;
74404
- }
74405
-
74406
- .myio-daterange-input {
74407
- max-width: 100%;
74408
- font-size: 16px; /* Prevents zoom on iOS */
74409
- }
74410
- }
74411
-
74412
- /* High contrast mode support */
74413
- @media (prefers-contrast: high) {
74414
- .myio-daterange-wrapper {
74415
- border: 2px solid #000;
74416
- }
74417
-
74418
- .myio-daterange-input {
74419
- border: 2px solid #000;
74420
- }
74421
- }
74422
-
74423
- /* Reduced motion support */
74424
- @media (prefers-reduced-motion: reduce) {
74425
- .myio-daterange-wrapper,
74426
- .myio-daterange-input,
74427
- .myio-daterange-helper {
74428
- transition: none;
74429
- }
74430
- }
74431
- `;
74432
- function injectPremiumStyles() {
74433
- const styleId = "myio-daterange-premium-styles";
74434
- if (document.getElementById(styleId)) {
74435
- return;
74436
- }
74437
- const styleEl = document.createElement("style");
74438
- styleEl.id = styleId;
74439
- styleEl.textContent = PREMIUM_STYLES;
74440
- document.head.appendChild(styleEl);
74441
- console.log("[MyIO] Premium date range styles injected");
74442
- }
74443
- function validateId(id, context) {
74444
- if (!id || typeof id !== "string") {
74445
- throw new Error(`[createInputDateRangePickerInsideDIV] ${context} must be a non-empty string`);
74446
- }
74447
- if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(id)) {
74448
- throw new Error(`[createInputDateRangePickerInsideDIV] ${context} '${id}' is not a valid HTML ID`);
74449
- }
74450
- }
74451
- async function createInputDateRangePickerInsideDIV(params) {
74452
- const {
74453
- containerId,
74454
- inputId,
74455
- label = "Per\xEDodo de Datas",
74456
- placeholder = "Clique para selecionar per\xEDodo",
74457
- pickerOptions = {},
74458
- classNames = {},
74459
- injectStyles: injectStyles23 = true,
74460
- showHelper = true
74461
- } = params;
74462
- validateId(containerId, "containerId");
74463
- validateId(inputId, "inputId");
74464
- const container = document.getElementById(containerId);
74465
- if (!container) {
74466
- throw new Error(`[createInputDateRangePickerInsideDIV] Container '#${containerId}' not found`);
74467
- }
74468
- if (injectStyles23) {
74469
- injectPremiumStyles();
74470
- }
74471
- let inputEl = document.getElementById(inputId);
74472
- if (inputEl && inputEl.tagName.toLowerCase() !== "input") {
74473
- throw new Error(`[createInputDateRangePickerInsideDIV] Element '#${inputId}' exists but is not an input element`);
74474
- }
74475
- const wrapper = document.createElement("div");
74476
- wrapper.className = classNames.wrapper || "myio-daterange-wrapper";
74477
- wrapper.setAttribute("data-myio-component", "daterange-input");
74478
- wrapper.setAttribute("data-version", "1.0.0");
74479
- let labelEl = null;
74480
- if (label) {
74481
- labelEl = document.createElement("label");
74482
- labelEl.className = classNames.label || "myio-daterange-label";
74483
- labelEl.textContent = label;
74484
- labelEl.setAttribute("for", inputId);
74485
- wrapper.appendChild(labelEl);
74486
- }
74487
- if (!inputEl) {
74488
- inputEl = document.createElement("input");
74489
- inputEl.type = "text";
74490
- inputEl.id = inputId;
74491
- inputEl.name = inputId;
74492
- }
74493
- inputEl.className = classNames.input || "myio-daterange-input";
74494
- inputEl.readOnly = true;
74495
- inputEl.placeholder = placeholder;
74496
- inputEl.autocomplete = "off";
74497
- inputEl.setAttribute("aria-label", label || "Date range selector");
74498
- if (showHelper) {
74499
- inputEl.setAttribute("aria-describedby", `${inputId}-helper`);
74500
- }
74501
- let helperEl = null;
74502
- if (showHelper) {
74503
- helperEl = document.createElement("div");
74504
- helperEl.id = `${inputId}-helper`;
74505
- helperEl.className = classNames.helper || "myio-daterange-helper";
74506
- helperEl.setAttribute("aria-live", "polite");
74507
- helperEl.style.display = "flex";
74508
- helperEl.style.alignItems = "center";
74509
- }
74510
- wrapper.appendChild(inputEl);
74511
- if (helperEl) {
74512
- wrapper.appendChild(helperEl);
74513
- }
74514
- const existingWrapper = container.querySelector('[data-myio-component="daterange-input"]');
74515
- if (existingWrapper) {
74516
- console.warn(`[createInputDateRangePickerInsideDIV] Replacing existing daterange input in container '#${containerId}'`);
74517
- existingWrapper.remove();
74518
- }
74519
- container.appendChild(wrapper);
74520
- const enhancedOptions = {
74521
- maxRangeDays: 31,
74522
- onApply: (result) => {
74523
- if (helperEl) {
74524
- const startDate = new Date(result.startISO);
74525
- const endDate = new Date(result.endISO);
74526
- const days = Math.ceil((endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24)) + 1;
74527
- helperEl.textContent = `Per\xEDodo selecionado: ${days} dia${days !== 1 ? "s" : ""}`;
74528
- helperEl.className = (classNames.helper || "myio-daterange-helper") + " success";
74529
- setTimeout(() => {
74530
- if (helperEl) {
74531
- helperEl.className = classNames.helper || "myio-daterange-helper";
74532
- }
74533
- }, 3e3);
74534
- }
74535
- if (pickerOptions.onApply) {
74536
- pickerOptions.onApply(result);
74537
- }
74538
- },
74539
- ...pickerOptions
74540
- };
74541
- let picker;
74542
- try {
74543
- picker = await createDateRangePicker2(inputEl, enhancedOptions);
74544
- console.log(`[createInputDateRangePickerInsideDIV] Successfully initialized for input '#${inputId}'`);
74545
- } catch (error) {
74546
- wrapper.remove();
74547
- throw new Error(`[createInputDateRangePickerInsideDIV] Failed to initialize date picker: ${error.message}`);
74548
- }
74549
- const controller = {
74550
- input: inputEl,
74551
- container,
74552
- wrapper,
74553
- picker,
74554
- getDisplayValue: () => inputEl.value,
74555
- getDates: () => picker.getDates(),
74556
- setDates: (startISO, endISO) => {
74557
- try {
74558
- picker.setDates(startISO, endISO);
74559
- } catch (error) {
74560
- console.error(`[createInputDateRangePickerInsideDIV] Error setting dates:`, error);
74561
- throw error;
74562
- }
74563
- },
74564
- setHelperText: (text2, type = "default") => {
74565
- if (helperEl) {
74566
- helperEl.textContent = text2;
74567
- const baseClass = classNames.helper || "myio-daterange-helper";
74568
- helperEl.className = type === "default" ? baseClass : `${baseClass} ${type}`;
74569
- }
74570
- },
74571
- destroy: () => {
74572
- try {
74573
- picker.destroy();
74574
- console.log(`[createInputDateRangePickerInsideDIV] Date picker destroyed for input '#${inputId}'`);
74575
- } catch (error) {
74576
- console.warn(`[createInputDateRangePickerInsideDIV] Error destroying picker:`, error);
74577
- }
74578
- try {
74579
- wrapper.remove();
74580
- console.log(`[createInputDateRangePickerInsideDIV] Wrapper removed for input '#${inputId}'`);
74581
- } catch (error) {
74582
- console.warn(`[createInputDateRangePickerInsideDIV] Error removing wrapper:`, error);
74583
- }
74584
- }
74585
- };
74586
- return controller;
74587
- }
74588
-
74589
75733
  // src/utils/goalsCoverage.ts
74590
75734
  var MONTHS_PT = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
74591
75735
  function formatCoverageRefPtBR(ref) {
@@ -101579,7 +102723,7 @@ async function openClearGcdrIdsModal(state6) {
101579
102723
  if (!state6.selectedCustomer) return;
101580
102724
  const customerId = getEntityId(state6.selectedCustomer);
101581
102725
  const customerName = state6.selectedCustomer.name || state6.selectedCustomer.title || customerId;
101582
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
102726
+ const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
101583
102727
  const overlay = document.createElement("div");
101584
102728
  overlay.style.cssText = `
101585
102729
  position:fixed; inset:0; z-index:99999;
@@ -101691,7 +102835,7 @@ async function openClearGcdrIdsModal(state6) {
101691
102835
  const chunks = [];
101692
102836
  for (let i = 0; i < items.length; i += 10) chunks.push(items.slice(i, i + 10));
101693
102837
  for (let ci = 0; ci < chunks.length; ci++) {
101694
- if (ci > 0) await sleep(800);
102838
+ if (ci > 0) await sleep2(800);
101695
102839
  await Promise.all(chunks[ci].map(async (entity) => {
101696
102840
  const tbId = entity.id?.id || "";
101697
102841
  const name = entity.name || entity.label || tbId;
@@ -101741,7 +102885,7 @@ async function openClearGcdrIdsModal(state6) {
101741
102885
  const chunks = [];
101742
102886
  for (let i = 0; i < rows.length; i += 10) chunks.push(rows.slice(i, i + 10));
101743
102887
  for (let ci = 0; ci < chunks.length; ci++) {
101744
- if (ci > 0) await sleep(800);
102888
+ if (ci > 0) await sleep2(800);
101745
102889
  await Promise.all(chunks[ci].map(async (row) => {
101746
102890
  const keysToDelete = keys.filter((k) => row.present[k]);
101747
102891
  try {
@@ -104348,6 +105492,7 @@ ${errors.slice(0, 5).join("\n")}` + (errors.length > 5 ? `
104348
105492
  // src/components/card-badges/index.ts
104349
105493
  var ALARM_STYLES_ID = "myio-alarm-badge-styles";
104350
105494
  var TICKET_STYLES_ID = "myio-ticket-badge-styles";
105495
+ var INCIDENT_STYLES_ID = "myio-incident-badge-styles";
104351
105496
  function injectAlarmBadgeStyles() {
104352
105497
  if (document.getElementById(ALARM_STYLES_ID)) return;
104353
105498
  const s = document.createElement("style");
@@ -104421,6 +105566,32 @@ function injectTicketBadgeStyles() {
104421
105566
  `;
104422
105567
  document.head.appendChild(s);
104423
105568
  }
105569
+ function injectIncidentBadgeStyles() {
105570
+ if (document.getElementById(INCIDENT_STYLES_ID)) return;
105571
+ const s = document.createElement("style");
105572
+ s.id = INCIDENT_STYLES_ID;
105573
+ s.textContent = `
105574
+ /* Stacked directly under .myio-alarm-badge (same corner, same shape) */
105575
+ .myio-incident-badge {
105576
+ position: absolute;
105577
+ top: 28px;
105578
+ left: 6px;
105579
+ background: #7C3AED;
105580
+ color: #fff;
105581
+ border-radius: 10px;
105582
+ padding: 2px 5px;
105583
+ font-size: 10px;
105584
+ font-weight: 700;
105585
+ display: flex;
105586
+ align-items: center;
105587
+ gap: 2px;
105588
+ z-index: 10;
105589
+ pointer-events: none;
105590
+ line-height: 1.3;
105591
+ }
105592
+ `;
105593
+ document.head.appendChild(s);
105594
+ }
104424
105595
  function defaultAlarmCount(gcdrDeviceId) {
104425
105596
  const aso = window.AlarmServiceOrchestrator;
104426
105597
  return aso?.getAlarmCountForDevice?.(gcdrDeviceId) ?? 0;
@@ -104431,6 +105602,13 @@ function alarmTitle(count) {
104431
105602
  function ticketTitle(count) {
104432
105603
  return count > 0 ? `${count} chamado${count !== 1 ? "s" : ""} aberto${count !== 1 ? "s" : ""}` : "Chamados";
104433
105604
  }
105605
+ function defaultIncidentCount(gcdrDeviceId) {
105606
+ const iso = window.IncidentServiceOrchestrator;
105607
+ return iso?.getIncidentCountForDevice?.(gcdrDeviceId) ?? 0;
105608
+ }
105609
+ function incidentTitle(count) {
105610
+ return `${count} incidente${count !== 1 ? "s" : ""} de interpola\xE7\xE3o`;
105611
+ }
104434
105612
  function addAlarmBadge(cardElement, gcdrDeviceId, opts) {
104435
105613
  if (!cardElement || !gcdrDeviceId) return;
104436
105614
  if (cardElement.querySelector(`[data-alarm-device-id="${gcdrDeviceId}"]`)) return;
@@ -104516,6 +105694,36 @@ function refreshTicketBadges(opts) {
104516
105694
  }
104517
105695
  });
104518
105696
  }
105697
+ function addIncidentBadge(cardElement, gcdrDeviceId, opts) {
105698
+ if (!cardElement || !gcdrDeviceId) return;
105699
+ if (cardElement.querySelector(`[data-incident-device-id="${gcdrDeviceId}"]`)) return;
105700
+ const count = (opts?.getCount ?? defaultIncidentCount)(gcdrDeviceId);
105701
+ injectIncidentBadgeStyles();
105702
+ if (cardElement.style) cardElement.style.position = "relative";
105703
+ const badge = document.createElement("div");
105704
+ badge.className = "myio-incident-badge";
105705
+ badge.setAttribute("data-incident-device-id", gcdrDeviceId);
105706
+ badge.style.display = count > 0 ? "" : "none";
105707
+ badge.title = incidentTitle(count);
105708
+ badge.innerHTML = `<svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M12 2L1 21h22L12 2z"/></svg><span>${count > 99 ? "99+" : count}</span>`;
105709
+ cardElement.appendChild(badge);
105710
+ }
105711
+ function refreshIncidentBadges(opts) {
105712
+ const getCount = opts?.getCount ?? defaultIncidentCount;
105713
+ document.querySelectorAll(".myio-incident-badge[data-incident-device-id]").forEach((badge) => {
105714
+ const gcdrDeviceId = badge.getAttribute("data-incident-device-id");
105715
+ if (!gcdrDeviceId) return;
105716
+ const count = getCount(gcdrDeviceId);
105717
+ const span = badge.querySelector("span");
105718
+ if (count > 0) {
105719
+ badge.style.display = "";
105720
+ badge.title = incidentTitle(count);
105721
+ if (span) span.textContent = count > 99 ? "99+" : String(count);
105722
+ } else {
105723
+ badge.style.display = "none";
105724
+ }
105725
+ });
105726
+ }
104519
105727
 
104520
105728
  // src/components/premium-modals/dialog/styles.ts
104521
105729
  var FONT_LINK_ID = "myio-dialog-font-nunito";
@@ -163107,6 +164315,16 @@ var CSS4 = `
163107
164315
  justify-content:center;gap:2px;line-height:1;cursor:default;box-shadow:0 1px 3px rgba(0,0,0,.25);
163108
164316
  z-index:10;
163109
164317
  }
164318
+ /* Incident (RFC-0232) sits at the vertical midpoint of the left border \u2014
164319
+ below the alarm badge, above the ticket badge \u2014 reading as a 3-badge
164320
+ stack on the same edge regardless of card height (card vs. compact). */
164321
+ .myio-cscard__incident-badge{
164322
+ position:absolute;top:50%;left:-14px;transform:translateY(-50%);
164323
+ background:#7C3AED;color:#fff;border-radius:10px;min-width:22px;height:22px;
164324
+ padding:0 6px;font:700 10px 'Nunito', system-ui, sans-serif;display:flex;align-items:center;
164325
+ justify-content:center;gap:2px;line-height:1;cursor:default;box-shadow:0 1px 3px rgba(0,0,0,.25);
164326
+ z-index:10;
164327
+ }
163110
164328
  .myio-cscard__ticket-badge{
163111
164329
  position:absolute;bottom:18%;left:-14px;transform:translateY(50%);width:24px;height:24px;
163112
164330
  background:rgba(8,145,178,.12);color:#0891b2;border:1px solid rgba(8,145,178,.25);
@@ -163573,6 +164791,7 @@ var CentralStatusCard = class _CentralStatusCard {
163573
164791
  detachTitleTooltip = null;
163574
164792
  detachDivergenceTooltip = null;
163575
164793
  detachAlarmTooltip = null;
164794
+ detachIncidentTooltip = null;
163576
164795
  detachTicketTooltip = null;
163577
164796
  detachAnnotationTooltips = [];
163578
164797
  /** Latency from the PREVIOUS render, to derive the trend arrow next to
@@ -163637,6 +164856,7 @@ var CentralStatusCard = class _CentralStatusCard {
163637
164856
  this.detachTitleTooltip?.();
163638
164857
  this.detachDivergenceTooltip?.();
163639
164858
  this.detachAlarmTooltip?.();
164859
+ this.detachIncidentTooltip?.();
163640
164860
  this.detachTicketTooltip?.();
163641
164861
  this.detachAnnotationTooltips.forEach((fn) => fn());
163642
164862
  this.el.remove();
@@ -163957,6 +165177,12 @@ var CentralStatusCard = class _CentralStatusCard {
163957
165177
  <svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6V11c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>
163958
165178
  <span>${p.alarmCount > 99 ? "99+" : p.alarmCount}</span>
163959
165179
  </div>` : "";
165180
+ const incidentBadgeHtml = p.incidentCount && p.incidentCount > 0 ? `<div class="myio-cscard__incident-badge" data-role="incident-badge" aria-label="${escAttr6(
165181
+ `${p.incidentCount} incidente${p.incidentCount !== 1 ? "s" : ""} de interpola\xE7\xE3o`
165182
+ )}">
165183
+ <svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M12 2L1 21h22L12 2z"/></svg>
165184
+ <span>${p.incidentCount > 99 ? "99+" : p.incidentCount}</span>
165185
+ </div>` : "";
163960
165186
  const ticketBadgeHtml = p.ticketCount && p.ticketCount > 0 ? `<div class="myio-cscard__ticket-badge" data-role="ticket-badge" aria-label="${escAttr6(
163961
165187
  `${p.ticketCount} chamado${p.ticketCount !== 1 ? "s" : ""} aberto${p.ticketCount !== 1 ? "s" : ""}`
163962
165188
  )}">
@@ -163993,6 +165219,7 @@ var CentralStatusCard = class _CentralStatusCard {
163993
165219
  const compactEvidenceLine = variant === "compact" ? `<div class="myio-cscard__evidence-line">${devicesText ? escAttr6(devicesText) : ""}${probeText ? (devicesText ? " \xB7 " : "") + probeText : ""}</div>` : "";
163994
165220
  this.el.innerHTML = `
163995
165221
  ${alarmBadgeHtml}
165222
+ ${incidentBadgeHtml}
163996
165223
  ${ticketBadgeHtml}
163997
165224
  ${annotationBadgesHtml}
163998
165225
  <div class="myio-cscard__surface">
@@ -164096,6 +165323,8 @@ var CentralStatusCard = class _CentralStatusCard {
164096
165323
  this.detachDivergenceTooltip = null;
164097
165324
  this.detachAlarmTooltip?.();
164098
165325
  this.detachAlarmTooltip = null;
165326
+ this.detachIncidentTooltip?.();
165327
+ this.detachIncidentTooltip = null;
164099
165328
  this.detachTicketTooltip?.();
164100
165329
  this.detachTicketTooltip = null;
164101
165330
  this.detachAnnotationTooltips.forEach((fn) => fn());
@@ -164137,6 +165366,19 @@ var CentralStatusCard = class _CentralStatusCard {
164137
165366
  }));
164138
165367
  }
164139
165368
  }
165369
+ if (p.incidentCount && p.incidentCount > 0) {
165370
+ const incidentBadge = this.el.querySelector('[data-role="incident-badge"]');
165371
+ if (incidentBadge) {
165372
+ const count = p.incidentCount;
165373
+ this.detachIncidentTooltip = InfoTooltip.attach(incidentBadge, () => ({
165374
+ icon: "\u{1F53A}",
165375
+ title: en ? "Incidents" : "Incidentes",
165376
+ content: escAttr6(
165377
+ en ? `${count} interpolated reading${count !== 1 ? "s" : ""}` : `${count} incidente${count !== 1 ? "s" : ""} de interpola\xE7\xE3o`
165378
+ )
165379
+ }));
165380
+ }
165381
+ }
164140
165382
  if (p.ticketCount && p.ticketCount > 0) {
164141
165383
  const ticketBadge = this.el.querySelector('[data-role="ticket-badge"]');
164142
165384
  if (ticketBadge) {
@@ -164240,6 +165482,13 @@ var CentralStatusCard = class _CentralStatusCard {
164240
165482
  this.params.onAlarmBadgeClick?.({ id: this.params.id, source: "central-status-card" });
164241
165483
  });
164242
165484
  }
165485
+ const incidentBadge = this.el.querySelector('[data-role="incident-badge"]');
165486
+ if (incidentBadge && this.params.onIncidentBadgeClick) {
165487
+ incidentBadge.addEventListener("click", (e) => {
165488
+ e.stopPropagation();
165489
+ this.params.onIncidentBadgeClick?.({ id: this.params.id, source: "central-status-card" });
165490
+ });
165491
+ }
164243
165492
  const ticketBadge = this.el.querySelector('[data-role="ticket-badge"]');
164244
165493
  if (ticketBadge) {
164245
165494
  ticketBadge.addEventListener("click", (e) => {
@@ -164438,6 +165687,9 @@ var version = package_default.version || "0.0.0";
164438
165687
  FreshdeskClient,
164439
165688
  GRANULARITY_SELECTOR_CSS_PREFIX,
164440
165689
  GRID_SORT_OPTIONS,
165690
+ GcdrAnnotationsClient,
165691
+ GcdrAnnotationsConflictError,
165692
+ GcdrAnnotationsError,
164441
165693
  GoalsModal,
164442
165694
  GoalsMoneyApiError,
164443
165695
  GoalsMoneyClient,
@@ -164579,9 +165831,13 @@ var version = package_default.version || "0.0.0";
164579
165831
  WELCOME_DEFAULT_PALETTE,
164580
165832
  Water,
164581
165833
  WaterSummaryTooltip,
165834
+ adaptGcdrListToLegacyAnnotations,
165835
+ adaptGcdrToLegacyAnnotation,
165836
+ adaptLegacyToGcdrInput,
164582
165837
  addAlarmBadge,
164583
165838
  addDetectionContext,
164584
165839
  addFreshdeskTicketNote,
165840
+ addIncidentBadge,
164585
165841
  addNamespace,
164586
165842
  addTicketBadge,
164587
165843
  aggregateByDay,
@@ -164688,6 +165944,8 @@ var version = package_default.version || "0.0.0";
164688
165944
  createFilterModalComponent,
164689
165945
  createFooterComponent,
164690
165946
  createFreshdeskTicket,
165947
+ createGcdrAnnotationsAuthStrategy,
165948
+ createGcdrAnnotationsClient,
164691
165949
  createGoalsBarTooltip,
164692
165950
  createGoalsMoneyClient,
164693
165951
  createGranularitySelector,
@@ -165032,6 +166290,7 @@ var version = package_default.version || "0.0.0";
165032
166290
  recalculateDeviceStatus,
165033
166291
  recomputeDeviceGridPercentages,
165034
166292
  refreshAlarmBadges,
166293
+ refreshIncidentBadges,
165035
166294
  refreshTicketBadges,
165036
166295
  removeAlarmsNotificationsPanelStyles,
165037
166296
  removeBASDashboardStyles,