myio-js-library 0.1.6 → 0.1.8

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
@@ -34,7 +34,6 @@ __export(index_exports, {
34
34
  determineInterval: () => determineInterval,
35
35
  exportToCSV: () => exportToCSV,
36
36
  exportToCSVAll: () => exportToCSVAll,
37
- fetchWithRetry: () => fetchWithRetry,
38
37
  findValue: () => findValue,
39
38
  fmtPerc: () => fmtPerc,
40
39
  fmtPercLegacy: () => fmtPerc2,
@@ -50,18 +49,17 @@ __export(index_exports, {
50
49
  formatWaterVolumeM3: () => formatWaterVolumeM3,
51
50
  getAvailableContexts: () => getAvailableContexts,
52
51
  getDateRangeArray: () => getDateRangeArray,
53
- getEntityInfoAndAttributesTB: () => getEntityInfoAndAttributesTB,
54
52
  getSaoPauloISOString: () => getSaoPauloISOString,
55
53
  getSaoPauloISOStringFixed: () => getSaoPauloISOStringFixed,
56
54
  getValueByDatakey: () => getValueByDatakey,
57
55
  getValueByDatakeyLegacy: () => getValueByDatakeyLegacy,
58
56
  getWaterCategories: () => getWaterCategories,
59
57
  groupByDay: () => groupByDay,
60
- http: () => http,
61
58
  isWaterCategory: () => isWaterCategory,
62
59
  normalizeRecipients: () => normalizeRecipients,
63
60
  numbers: () => numbers_exports,
64
61
  parseInputDateToDate: () => parseInputDateToDate,
62
+ renderCardComponent: () => renderCardComponent,
65
63
  strings: () => strings_exports,
66
64
  timeWindowFromInputYMD: () => timeWindowFromInputYMD,
67
65
  toCSV: () => toCSV,
@@ -93,14 +91,26 @@ function formatEnergy(value, unit) {
93
91
  });
94
92
  return `${formattedValue} ${adjustedUnit}`;
95
93
  }
96
- function formatAllInSameUnit(values, targetUnit) {
94
+ function formatAllInSameUnit(values, targetUnit, sourceUnit = "kWh") {
97
95
  const unitMultipliers = {
98
96
  "kWh": 1,
99
97
  "MWh": 1e3,
100
98
  "GWh": 1e6
101
99
  };
102
100
  const targetMultiplier = unitMultipliers[targetUnit] || 1;
103
- return values.map((item) => {
101
+ if (typeof values[0] === "number") {
102
+ const numberValues = values;
103
+ const sourceMultiplier = unitMultipliers[sourceUnit] || 1;
104
+ return numberValues.map((value) => {
105
+ if (value === null || value === void 0 || isNaN(value)) {
106
+ return "-";
107
+ }
108
+ const convertedValue = value * sourceMultiplier / targetMultiplier;
109
+ return formatEnergy(convertedValue, targetUnit);
110
+ });
111
+ }
112
+ const objectValues = values;
113
+ return objectValues.map((item) => {
104
114
  if (item.value === null || item.value === void 0 || isNaN(item.value)) {
105
115
  return "-";
106
116
  }
@@ -725,69 +735,6 @@ function findValue(data, keyOrPath, legacyDataKey) {
725
735
  return getValueByDatakey(data, keyOrPath);
726
736
  }
727
737
 
728
- // src/thingsboard/entity.ts
729
- async function getEntityInfoAndAttributesTB(deviceId, opts) {
730
- if (!deviceId) throw new Error("getEntityInfoAndAttributesTB: deviceId is required");
731
- const {
732
- jwt,
733
- baseUrl = "",
734
- scope = "SERVER_SCOPE",
735
- attributeKeys = [
736
- "floor",
737
- "NumLoja",
738
- "IDMedidor",
739
- "deviceId",
740
- "guid",
741
- "maxDailyConsumption",
742
- "maxNightConsumption"
743
- ],
744
- fetcher = globalThis.fetch?.bind(globalThis)
745
- } = opts || {};
746
- if (!jwt) throw new Error("getEntityInfoAndAttributesTB: opts.jwt (Bearer token) is required");
747
- if (!fetcher) throw new Error("getEntityInfoAndAttributesTB: no fetch implementation available");
748
- const headers = {
749
- "Content-Type": "application/json",
750
- "X-Authorization": `Bearer ${jwt}`
751
- };
752
- const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
753
- const deviceRes = await fetcher(`${base}/api/device/${encodeURIComponent(deviceId)}`, { headers });
754
- if (!deviceRes.ok) {
755
- throw new Error(`Failed to fetch device: HTTP ${deviceRes.status} ${deviceRes.statusText}`);
756
- }
757
- const device = await deviceRes.json();
758
- const label = device?.label || device?.name || "Sem etiqueta";
759
- const attrUrl = `${base}/api/plugins/telemetry/DEVICE/${encodeURIComponent(deviceId)}/values/attributes?scope=${encodeURIComponent(scope)}`;
760
- const attrRes = await fetcher(attrUrl, { headers });
761
- if (!attrRes.ok) {
762
- throw new Error(`Failed to fetch attributes: HTTP ${attrRes.status} ${attrRes.statusText}`);
763
- }
764
- const attributes = await attrRes.json();
765
- const map = /* @__PURE__ */ new Map();
766
- for (const a of attributes) map.set(a.key, a.value);
767
- const getStr = (k) => {
768
- const v = map.get(k);
769
- if (v == null) return "";
770
- return typeof v === "string" ? v : String(v);
771
- };
772
- const getNum = (k) => {
773
- const v = map.get(k);
774
- if (v == null) return 0;
775
- const n = typeof v === "string" ? Number(v.replace(",", ".")) : Number(v);
776
- return Number.isFinite(n) ? n : 0;
777
- };
778
- void attributeKeys;
779
- return {
780
- label,
781
- andar: getStr("floor") || "",
782
- numeroLoja: getStr("NumLoja") || "",
783
- identificadorMedidor: getStr("IDMedidor") || "",
784
- identificadorDispositivo: getStr("deviceId") || "",
785
- guid: getStr("guid") || "",
786
- consumoDiario: getNum("maxDailyConsumption"),
787
- consumoMadrugada: getNum("maxNightConsumption")
788
- };
789
- }
790
-
791
738
  // src/utils/deviceType.js
792
739
  var contexts = {
793
740
  building: (name) => {
@@ -936,76 +883,164 @@ function base64ToBytesStrict(b64) {
936
883
  return out;
937
884
  }
938
885
 
939
- // src/net/http.js
940
- async function fetchWithRetry(url, options = {}) {
886
+ // src/thingsboard/main-dashboard-shopping/v-4.0.0/card/template-card.js
887
+ function renderCardComponent({
888
+ entityObject,
889
+ handleActionDashboard,
890
+ handleActionReport,
891
+ handleActionSettings,
892
+ handleSelect
893
+ }) {
941
894
  const {
942
- retries = 0,
943
- retryDelay = 100,
944
- timeout = 1e4,
945
- retryCondition,
946
- ...passThrough
947
- } = options;
948
- const baseInit = { ...passThrough, timeout };
949
- let attempt = 0;
950
- while (true) {
951
- try {
952
- const res = await withTimeout(fetch(url, baseInit), timeout);
953
- if (!res.ok) {
954
- const doRetry = typeof retryCondition === "function" && retryCondition(null, res) || res.status >= 500;
955
- if (doRetry && attempt < retries) {
956
- await delay(expBackoff(retryDelay, attempt));
957
- attempt++;
958
- continue;
959
- }
960
- const msg = `HTTP ${res.status}: ${res.statusText || ""}`.trim();
961
- throw new Error(msg);
962
- }
963
- return res;
964
- } catch (err) {
965
- if (err && err.message === "Request timeout") {
966
- if (attempt < retries) {
967
- await delay(expBackoff(retryDelay, attempt));
968
- attempt++;
969
- continue;
970
- }
971
- throw err;
972
- }
973
- const doRetry = typeof retryCondition === "function" && retryCondition(err, void 0) || isRetryableNetworkError(err);
974
- if (doRetry && attempt < retries) {
975
- await delay(expBackoff(retryDelay, attempt));
976
- attempt++;
977
- continue;
978
- }
979
- throw err;
980
- }
981
- }
895
+ entityId,
896
+ labelOrName,
897
+ entityType,
898
+ slaveId,
899
+ ingestionId,
900
+ val,
901
+ centralId,
902
+ updatedIdentifiers = {},
903
+ img,
904
+ isOn = false,
905
+ perc = 0,
906
+ group,
907
+ connectionStatus = "online"
908
+ } = entityObject;
909
+ const MyIO = typeof MyIOLibrary !== "undefined" && MyIOLibrary || typeof window !== "undefined" && window.MyIOLibrary || {
910
+ formatEnergyByGroup: (v, g) => `${v} kWh${g ? " \xB7 " + g : ""}`,
911
+ formatNumberReadable: (n) => Number(n ?? 0).toFixed(1)
912
+ };
913
+ const valFormatted = MyIO.formatEnergyByGroup(val, group);
914
+ const percFormatted = MyIO.formatNumberReadable(perc);
915
+ if (!document.getElementById("myio-card-styles")) {
916
+ const style = document.createElement("style");
917
+ style.id = "myio-card-styles";
918
+ style.textContent = `
919
+ .device-card-centered,.clickable{width:98%;border-radius:10px;padding:8px 12px;background:#fff;
920
+ box-shadow:0 4px 10px rgba(0,0,0,.05);display:flex;align-items:center;justify-content:flex-start;
921
+ cursor:pointer;transition:transform .2s;position:relative;min-height:140px;box-sizing:border-box;gap:25px;
922
+ overflow:hidden;margin-bottom:15px}
923
+ .device-card-centered:hover,.clickable:hover{transform:scale(1.05)}
924
+ .device-title-row{width:100%;display:flex;justify-content:center;align-items:center;margin-bottom:4px;padding:0 4px;min-height:22px}
925
+ .device-title{font-weight:700;font-size:.85rem;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:90%;line-height:1.1}
926
+ .device-image{max-height:44px;width:auto;margin:4px 0;display:block}
927
+ .device-data-row{display:flex;justify-content:center;align-items:center;margin-top:auto;margin-bottom:6px;gap:6px;width:100%}
928
+ .consumption-main{font-size:.9rem;font-weight:700;color:#28a745;display:flex;align-items:center;gap:6px;justify-content:center;white-space:nowrap}
929
+ .device-title-percent{font-size:.75rem;color:rgba(0,0,0,.45);font-weight:500}
930
+ .flash{animation:flash 1s infinite;color:#ff9800}
931
+ @keyframes flash{0%{opacity:1}50%{opacity:.2}100%{opacity:1}}
932
+ .card-actions{gap:13px;width:36px;padding:5px;height:100%;box-shadow:1px 0 2px rgba(0,0,0,.1);
933
+ display:flex;flex-direction:column;justify-content:center;align-items:center}
934
+ .card-action img{width:24px;height:24px;transition:transform .2s ease;cursor:pointer}
935
+ .card-action img:hover{transform:scale(1.15)}
936
+ .device-card-centered.offline {
937
+ border: 2px solid #ff4d4f;
938
+ animation: border-blink 1s infinite;
982
939
  }
983
- var http = fetchWithRetry;
984
- function withTimeout(promise, ms) {
985
- return new Promise((resolve, reject) => {
986
- const t = setTimeout(() => reject(new Error("Request timeout")), ms);
987
- promise.then(
988
- (v) => {
989
- clearTimeout(t);
990
- resolve(v);
991
- },
992
- (e) => {
993
- clearTimeout(t);
994
- reject(e);
995
- }
996
- );
997
- });
940
+
941
+ @keyframes border-blink {
942
+ 0%, 100% { box-shadow: 0 0 8px rgba(255, 77, 79, 0.9); }
943
+ 50% { box-shadow: 0 0 16px rgba(255, 0, 0, 0.6); }
998
944
  }
999
- function delay(ms) {
1000
- return new Promise((r) => setTimeout(r, ms));
945
+
946
+ .device-card-centered.offline .flash-icon {
947
+ color: #ff4d4f !important;
948
+ font-size: 1.2rem;
949
+ }
950
+
951
+ .flash-icon.flash {
952
+ animation: icon-blink 1s infinite;
1001
953
  }
1002
- function expBackoff(base, attempt) {
1003
- return base * Math.pow(2, attempt);
954
+
955
+ @keyframes icon-blink {
956
+ 0%, 100% { opacity: 1; transform: scale(1); }
957
+ 50% { opacity: 0.2; transform: scale(1.2); }
1004
958
  }
1005
- function isRetryableNetworkError(err) {
1006
- if (!err) return false;
1007
- const msg = String(err.message || "").toLowerCase();
1008
- return msg.includes("network") || err.name === "AbortError";
959
+
960
+ .device-card-centered.offline .flash-icon {
961
+ color: #ff4d4f !important;
962
+ }
963
+
964
+ .device-card-centered.online .flash-icon {
965
+ color: #28a745 !important; /* verde premium para online */
966
+ }
967
+ `;
968
+ document.head.appendChild(style);
969
+ }
970
+ const html = `
971
+ <div class="device-card-centered clickable ${connectionStatus === "offline" ? "offline" : ""}"
972
+ data-entity-id="${entityId}"
973
+ data-entity-label="${labelOrName}"
974
+ data-entity-type="${entityType}"
975
+ data-entity-slaveid="${slaveId}"
976
+ data-entity-ingestionid="${ingestionId}"
977
+ data-entity-consumption="${val}"
978
+ data-entity-centralid="${centralId}"
979
+ data-entity-updated-identifiers='${JSON.stringify(updatedIdentifiers)}'>
980
+
981
+ <div class="card-actions">
982
+ <div class="card-action action-dashboard" data-action="dashboard" title="Dashboard">
983
+ <img src="https://dashboard.myio-bas.com/api/images/public/TAVXE0sTbCZylwGsMF9lIWdllBB3iFtS"/>
984
+ </div>
985
+ <div class="card-action action-report" data-action="report" title="Relat\xF3rio">
986
+ <img src="https://dashboard.myio-bas.com/api/images/public/d9XuQwMYQCG2otvtNSlqUHGavGaSSpz4"/>
987
+ </div>
988
+ <div class="card-action action-settings" data-action="settings" title="Configura\xE7\xF5es">
989
+ <img src="https://dashboard.myio-bas.com/api/images/public/5n9tze6vED2uwIs5VvJxGzNNZ9eV4yoz"/>
990
+ </div>
991
+ <input class="card-action action-checker" data-action="checker" title="Selecionar" type="checkbox">
992
+ </div>
993
+
994
+ <div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;width:85%">
995
+ <div class="device-title-row">
996
+ <span class="device-title" title="${labelOrName}">
997
+ ${String(labelOrName ?? "").length > 15 ? String(labelOrName).slice(0, 15) + "\u2026" : String(labelOrName ?? "")}
998
+ </span>
999
+ </div>
1000
+ <img class="device-image ${isOn ? "flash" : ""}" src="${img || ""}" />
1001
+ <div class="device-data-row">
1002
+ <div class="consumption-main">
1003
+ <span class="flash-icon ${connectionStatus === "offline" ? "flash" : isOn ? "flash" : ""}">
1004
+ ${connectionStatus === "offline" ? "\u{1F6A8}" : "\u26A1"}
1005
+ </span>
1006
+ <span class="consumption-value" data-entity-consumption="${val}">${valFormatted}</span>
1007
+ <span class="device-title-percent">(${percFormatted}%)</span>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+ </div>
1012
+ `;
1013
+ const $card = $(html);
1014
+ if (typeof handleActionDashboard === "function") {
1015
+ $card.find(".action-dashboard").on("click", (e) => {
1016
+ e.stopPropagation();
1017
+ handleActionDashboard(entityObject);
1018
+ });
1019
+ }
1020
+ if (typeof handleActionReport === "function") {
1021
+ $card.find(".action-report").on("click", (e) => {
1022
+ e.stopPropagation();
1023
+ handleActionReport(entityObject);
1024
+ });
1025
+ }
1026
+ if (typeof handleActionSettings === "function") {
1027
+ $card.find(".action-settings").on("click", (e) => {
1028
+ e.stopPropagation();
1029
+ handleActionSettings(entityObject);
1030
+ });
1031
+ }
1032
+ if (typeof handleSelect === "function") {
1033
+ $card.on("click", (e) => {
1034
+ if (!$(e.target).closest(".card-action").length) {
1035
+ handleSelect(entityObject);
1036
+ }
1037
+ });
1038
+ $card.find(".action-checker").on("click", (e) => {
1039
+ e.stopPropagation();
1040
+ handleSelect(entityObject);
1041
+ });
1042
+ }
1043
+ return $card;
1009
1044
  }
1010
1045
  // Annotate the CommonJS export names for ESM import in node:
1011
1046
  0 && (module.exports = {
@@ -1024,7 +1059,6 @@ function isRetryableNetworkError(err) {
1024
1059
  determineInterval,
1025
1060
  exportToCSV,
1026
1061
  exportToCSVAll,
1027
- fetchWithRetry,
1028
1062
  findValue,
1029
1063
  fmtPerc,
1030
1064
  fmtPercLegacy,
@@ -1040,18 +1074,17 @@ function isRetryableNetworkError(err) {
1040
1074
  formatWaterVolumeM3,
1041
1075
  getAvailableContexts,
1042
1076
  getDateRangeArray,
1043
- getEntityInfoAndAttributesTB,
1044
1077
  getSaoPauloISOString,
1045
1078
  getSaoPauloISOStringFixed,
1046
1079
  getValueByDatakey,
1047
1080
  getValueByDatakeyLegacy,
1048
1081
  getWaterCategories,
1049
1082
  groupByDay,
1050
- http,
1051
1083
  isWaterCategory,
1052
1084
  normalizeRecipients,
1053
1085
  numbers,
1054
1086
  parseInputDateToDate,
1087
+ renderCardComponent,
1055
1088
  strings,
1056
1089
  timeWindowFromInputYMD,
1057
1090
  toCSV,
package/dist/index.d.cts CHANGED
@@ -7,14 +7,15 @@
7
7
  declare function formatEnergy(value: number, unit?: string): string;
8
8
  /**
9
9
  * Formats all energy values to the same unit for consistent display
10
- * @param values - Array of energy values with their units
10
+ * @param values - Array of energy values with their units OR array of numbers (assumes kWh)
11
11
  * @param targetUnit - Target unit to convert all values to ('kWh', 'MWh', 'GWh')
12
+ * @param sourceUnit - Source unit when values is an array of numbers (defaults to 'kWh')
12
13
  * @returns Array of formatted energy strings in the target unit
13
14
  */
14
15
  declare function formatAllInSameUnit(values: Array<{
15
16
  value: number;
16
17
  unit: string;
17
- }>, targetUnit: string): string[];
18
+ }> | number[], targetUnit: string, sourceUnit?: string): string[];
18
19
 
19
20
  /**
20
21
  * Formats a percentage value with Brazilian locale formatting
@@ -308,37 +309,6 @@ declare function getValueByDatakeyLegacy(dataList: any[], dataSourceNameTarget:
308
309
  */
309
310
  declare function findValue(data: any, keyOrPath: string, legacyDataKey?: string): any;
310
311
 
311
- /**
312
- * ThingsBoard entity and attributes fetching utilities
313
- */
314
- interface TBFetchOptions {
315
- jwt: string;
316
- baseUrl?: string;
317
- scope?: 'SERVER_SCOPE' | 'CLIENT_SCOPE' | 'SHARED_SCOPE';
318
- attributeKeys?: string[];
319
- fetcher?: typeof fetch;
320
- }
321
- interface TBEntityInfo {
322
- label: string;
323
- andar: string;
324
- numeroLoja: string;
325
- identificadorMedidor: string;
326
- identificadorDispositivo: string;
327
- guid: string;
328
- consumoDiario: number;
329
- consumoMadrugada: number;
330
- }
331
- /**
332
- * Fetches ThingsBoard Device info + attributes (default: SERVER_SCOPE).
333
- * Safe defaults and robust coercion, suitable for direct UI use.
334
- *
335
- * @param deviceId - The ThingsBoard device ID
336
- * @param opts - Configuration options including JWT token and API settings
337
- * @returns Promise resolving to device info and attributes
338
- * @throws Error if deviceId is missing, JWT is missing, or HTTP requests fail
339
- */
340
- declare function getEntityInfoAndAttributesTB(deviceId: string, opts: TBFetchOptions): Promise<TBEntityInfo>;
341
-
342
312
  /**
343
313
  * Detects the device type based on the given name and context.
344
314
  * Uses the specified detection context to identify device types.
@@ -406,31 +376,12 @@ declare namespace strings {
406
376
  declare function decodePayload(encoded: any, key: any): string;
407
377
  declare function decodePayloadBase64Xor(encoded: any, xorKey?: number): string;
408
378
 
409
- /**
410
- * fetchWithRetry(url, {
411
- * retries?: number = 0,
412
- * retryDelay?: number = 100,
413
- * timeout?: number = 10000,
414
- * retryCondition?: (error, response) => boolean,
415
- * ...RequestInit (method, headers, body, signal, etc.)
416
- * })
417
- *
418
- * Observação: os testes verificam que o objeto de options passado ao fetch
419
- * contém a prop `timeout`, então mantemos `{ timeout }` no init.
420
- */
421
- declare function fetchWithRetry(url: any, options?: {}): Promise<any>;
422
- /**
423
- * fetchWithRetry(url, {
424
- * retries?: number = 0,
425
- * retryDelay?: number = 100,
426
- * timeout?: number = 10000,
427
- * retryCondition?: (error, response) => boolean,
428
- * ...RequestInit (method, headers, body, signal, etc.)
429
- * })
430
- *
431
- * Observação: os testes verificam que o objeto de options passado ao fetch
432
- * contém a prop `timeout`, então mantemos `{ timeout }` no init.
433
- */
434
- declare function http(url: any, options?: {}): Promise<any>;
379
+ declare function renderCardComponent({ entityObject, handleActionDashboard, handleActionReport, handleActionSettings, handleSelect }: {
380
+ entityObject: any;
381
+ handleActionDashboard: any;
382
+ handleActionReport: any;
383
+ handleActionSettings: any;
384
+ handleSelect: any;
385
+ }): any;
435
386
 
436
- export { type StoreRow, type TBEntityInfo, type TBFetchOptions, type TimedValue, type WaterRow, addDetectionContext, addNamespace, averageByDay, buildWaterReportCSV, buildWaterStoresCSV, calcDeltaPercent, classify, classifyWaterLabel, classifyWaterLabels, decodePayload, decodePayloadBase64Xor, detectDeviceType, determineInterval, exportToCSV, exportToCSVAll, fetchWithRetry, findValue, fmtPerc$1 as fmtPerc, fmtPerc as fmtPercLegacy, formatAllInSameUnit, formatAllInSameWaterUnit, formatDateForInput, formatDateToYMD, formatDateWithTimezoneOffset, formatEnergy, formatEnergyByGroup, formatNumberReadable, formatTankHeadFromCm, formatWaterVolumeM3, getAvailableContexts, getDateRangeArray, getEntityInfoAndAttributesTB, getSaoPauloISOString, getSaoPauloISOStringFixed, getValueByDatakey, getValueByDatakeyLegacy, getWaterCategories, groupByDay, http, isWaterCategory, normalizeRecipients, numbers, parseInputDateToDate, strings, timeWindowFromInputYMD, toCSV, toFixedSafe };
387
+ export { type StoreRow, type TimedValue, type WaterRow, addDetectionContext, addNamespace, averageByDay, buildWaterReportCSV, buildWaterStoresCSV, calcDeltaPercent, classify, classifyWaterLabel, classifyWaterLabels, decodePayload, decodePayloadBase64Xor, detectDeviceType, determineInterval, exportToCSV, exportToCSVAll, findValue, fmtPerc$1 as fmtPerc, fmtPerc as fmtPercLegacy, formatAllInSameUnit, formatAllInSameWaterUnit, formatDateForInput, formatDateToYMD, formatDateWithTimezoneOffset, formatEnergy, formatEnergyByGroup, formatNumberReadable, formatTankHeadFromCm, formatWaterVolumeM3, getAvailableContexts, getDateRangeArray, getSaoPauloISOString, getSaoPauloISOStringFixed, getValueByDatakey, getValueByDatakeyLegacy, getWaterCategories, groupByDay, isWaterCategory, normalizeRecipients, numbers, parseInputDateToDate, renderCardComponent, strings, timeWindowFromInputYMD, toCSV, toFixedSafe };