hearald 0.1.2 → 0.1.3
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/CHANGELOG.md +7 -0
- package/README.md +2 -0
- package/dist/analytics.html +57 -16
- package/dist/index.js +57 -16
- package/package.json +1 -1
- package/src/public/js/App.js +13 -16
- package/src/public/js/util.js +12 -0
- package/src/shared/utils.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.3] - 2026-11-08
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fix the dashboard chart's initial loading state when the app does not have a `users` field.
|
|
15
|
+
- Fix the dashboard's timezone to match the server's daily bucketing (Pacific time).
|
|
16
|
+
|
|
10
17
|
## [0.1.2] - 2026-10-26
|
|
11
18
|
|
|
12
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -117,6 +117,8 @@ const statDefinitions = {
|
|
|
117
117
|
list of tiles in `fields` (or `stats`); a tile is `{ key, label, type }` with `type` one of `number`,
|
|
118
118
|
`percent` or `histogram`, and an optional `span`. `chartContexts` (`[{ id, menuName }]`) fills the chart's
|
|
119
119
|
"Context" menu. Both are optional; a dashboard with no `ui` shows only the chart.
|
|
120
|
+
The chart starts on the `users` field and the `all` context when the app has them, otherwise on the
|
|
121
|
+
first field and the first `chartContexts` entry. The page's date defaults to today in Pacific time.
|
|
120
122
|
|
|
121
123
|
Names with dots become nested objects in the result. `hidden: true` leaves a metric out of the output
|
|
122
124
|
(useful for sets that only feed a computed metric). A computed metric that names a formula that does
|
package/dist/analytics.html
CHANGED
|
@@ -405,6 +405,36 @@ th {
|
|
|
405
405
|
|
|
406
406
|
var m=e.bind(_);
|
|
407
407
|
|
|
408
|
+
// A Date whose UTC fields hold the current Pacific wall-clock time, so `iso()` (which reads UTC
|
|
409
|
+
// fields) names the Pacific day. The instant it represents is not "now"; it is only for day buckets.
|
|
410
|
+
const nowAsPstDate = (now = new Date()) => {
|
|
411
|
+
const parts = Object.fromEntries(
|
|
412
|
+
new Intl.DateTimeFormat("en-US", {
|
|
413
|
+
timeZone: "America/Los_Angeles",
|
|
414
|
+
hourCycle: "h23",
|
|
415
|
+
year: "numeric",
|
|
416
|
+
month: "numeric",
|
|
417
|
+
day: "numeric",
|
|
418
|
+
hour: "numeric",
|
|
419
|
+
minute: "numeric",
|
|
420
|
+
second: "numeric",
|
|
421
|
+
})
|
|
422
|
+
.formatToParts(now)
|
|
423
|
+
.map((p) => [p.type, p.value])
|
|
424
|
+
);
|
|
425
|
+
return new Date(
|
|
426
|
+
Date.UTC(
|
|
427
|
+
Number(parts.year),
|
|
428
|
+
Number(parts.month) - 1,
|
|
429
|
+
Number(parts.day),
|
|
430
|
+
Number(parts.hour),
|
|
431
|
+
Number(parts.minute),
|
|
432
|
+
Number(parts.second),
|
|
433
|
+
now.getMilliseconds()
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
|
|
408
438
|
function iso(time) {
|
|
409
439
|
return [
|
|
410
440
|
time.getUTCFullYear(),
|
|
@@ -413,6 +443,10 @@ th {
|
|
|
413
443
|
].join("-");
|
|
414
444
|
}
|
|
415
445
|
|
|
446
|
+
// Today's calendar date in Pacific time as YYYY-MM-DD (daylight saving included), the same day
|
|
447
|
+
// buckets the server uses. The dashboard page defaults its date to this, not the browser's UTC date.
|
|
448
|
+
const pacificToday = (now = new Date()) => iso(nowAsPstDate(now));
|
|
449
|
+
|
|
416
450
|
/* START methods for fetching data from the server */
|
|
417
451
|
|
|
418
452
|
const dataCache = {};
|
|
@@ -485,6 +519,18 @@ th {
|
|
|
485
519
|
|
|
486
520
|
/* END methods for fetching data from the server */
|
|
487
521
|
|
|
522
|
+
// Keeps the preferred chart field/context when they exist, otherwise falls back to the first
|
|
523
|
+
// available one, so the menus (which show the first option when nothing matches) and the chart agree.
|
|
524
|
+
function resolveChartDefaults({ chartField, chartContext, stats, contexts }) {
|
|
525
|
+
const fields = Object.keys(stats?.metrics?.all || {});
|
|
526
|
+
const contextIds = (contexts || []).map((c) => c.id);
|
|
527
|
+
return {
|
|
528
|
+
chartField: fields.length && !fields.includes(chartField) ? fields[0] : chartField,
|
|
529
|
+
chartContext:
|
|
530
|
+
contextIds.length && !contextIds.includes(chartContext) ? contextIds[0] : chartContext,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
488
534
|
/* START helper methods */
|
|
489
535
|
|
|
490
536
|
const getJson = async (url) => {
|
|
@@ -622,8 +668,6 @@ th {
|
|
|
622
668
|
}
|
|
623
669
|
}
|
|
624
670
|
|
|
625
|
-
const DATE = new Date().toISOString().split("T")[0];
|
|
626
|
-
|
|
627
671
|
const Logo = () => m`
|
|
628
672
|
<svg
|
|
629
673
|
width="256"
|
|
@@ -703,7 +747,7 @@ th {
|
|
|
703
747
|
super(props);
|
|
704
748
|
this.state = {
|
|
705
749
|
numDays: 30,
|
|
706
|
-
date:
|
|
750
|
+
date: pacificToday(),
|
|
707
751
|
chartField: "users",
|
|
708
752
|
chartContext: "all",
|
|
709
753
|
uiSectionsLoaded: false,
|
|
@@ -713,25 +757,22 @@ th {
|
|
|
713
757
|
async componentDidMount() {
|
|
714
758
|
await fetchUI();
|
|
715
759
|
this.setState({ uiSectionsLoaded: true });
|
|
716
|
-
|
|
760
|
+
// The chart waits for the stats so its field and context can be checked against them.
|
|
761
|
+
let stats = null;
|
|
762
|
+
await fetchData({
|
|
717
763
|
numDays: this.state.numDays,
|
|
718
764
|
date: this.state.date,
|
|
719
765
|
getDataFn,
|
|
720
|
-
onLoadComplete: (
|
|
766
|
+
onLoadComplete: (s) => (stats = s),
|
|
721
767
|
});
|
|
722
|
-
|
|
723
|
-
numDays: this.state.numDays,
|
|
724
|
-
date: this.state.date,
|
|
725
|
-
chartContext: this.state.chartContext,
|
|
768
|
+
const { chartField, chartContext } = resolveChartDefaults({
|
|
726
769
|
chartField: this.state.chartField,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
},
|
|
731
|
-
onLoadComplete: (data, max) => {
|
|
732
|
-
this.setState({ chartData: { loading: false, data, max } });
|
|
733
|
-
},
|
|
770
|
+
chartContext: this.state.chartContext,
|
|
771
|
+
stats,
|
|
772
|
+
contexts: UI.chartContexts,
|
|
734
773
|
});
|
|
774
|
+
this.setState({ stats, chartField, chartContext });
|
|
775
|
+
this.fetchChartData({ chartField, chartContext });
|
|
735
776
|
}
|
|
736
777
|
|
|
737
778
|
async fetchChartData({ numDays, date, chartContext, chartField }) {
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
|
|
8
8
|
var m=e.bind(_);
|
|
9
9
|
|
|
10
|
+
// A Date whose UTC fields hold the current Pacific wall-clock time, so `iso()` (which reads UTC
|
|
11
|
+
// fields) names the Pacific day. The instant it represents is not "now"; it is only for day buckets.
|
|
12
|
+
const nowAsPstDate = (now = new Date()) => {
|
|
13
|
+
const parts = Object.fromEntries(
|
|
14
|
+
new Intl.DateTimeFormat("en-US", {
|
|
15
|
+
timeZone: "America/Los_Angeles",
|
|
16
|
+
hourCycle: "h23",
|
|
17
|
+
year: "numeric",
|
|
18
|
+
month: "numeric",
|
|
19
|
+
day: "numeric",
|
|
20
|
+
hour: "numeric",
|
|
21
|
+
minute: "numeric",
|
|
22
|
+
second: "numeric",
|
|
23
|
+
})
|
|
24
|
+
.formatToParts(now)
|
|
25
|
+
.map((p) => [p.type, p.value])
|
|
26
|
+
);
|
|
27
|
+
return new Date(
|
|
28
|
+
Date.UTC(
|
|
29
|
+
Number(parts.year),
|
|
30
|
+
Number(parts.month) - 1,
|
|
31
|
+
Number(parts.day),
|
|
32
|
+
Number(parts.hour),
|
|
33
|
+
Number(parts.minute),
|
|
34
|
+
Number(parts.second),
|
|
35
|
+
now.getMilliseconds()
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
10
40
|
function iso(time) {
|
|
11
41
|
return [
|
|
12
42
|
time.getUTCFullYear(),
|
|
@@ -15,6 +45,10 @@
|
|
|
15
45
|
].join("-");
|
|
16
46
|
}
|
|
17
47
|
|
|
48
|
+
// Today's calendar date in Pacific time as YYYY-MM-DD (daylight saving included), the same day
|
|
49
|
+
// buckets the server uses. The dashboard page defaults its date to this, not the browser's UTC date.
|
|
50
|
+
const pacificToday = (now = new Date()) => iso(nowAsPstDate(now));
|
|
51
|
+
|
|
18
52
|
/* START methods for fetching data from the server */
|
|
19
53
|
|
|
20
54
|
const dataCache = {};
|
|
@@ -87,6 +121,18 @@
|
|
|
87
121
|
|
|
88
122
|
/* END methods for fetching data from the server */
|
|
89
123
|
|
|
124
|
+
// Keeps the preferred chart field/context when they exist, otherwise falls back to the first
|
|
125
|
+
// available one, so the menus (which show the first option when nothing matches) and the chart agree.
|
|
126
|
+
function resolveChartDefaults({ chartField, chartContext, stats, contexts }) {
|
|
127
|
+
const fields = Object.keys(stats?.metrics?.all || {});
|
|
128
|
+
const contextIds = (contexts || []).map((c) => c.id);
|
|
129
|
+
return {
|
|
130
|
+
chartField: fields.length && !fields.includes(chartField) ? fields[0] : chartField,
|
|
131
|
+
chartContext:
|
|
132
|
+
contextIds.length && !contextIds.includes(chartContext) ? contextIds[0] : chartContext,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
90
136
|
/* START helper methods */
|
|
91
137
|
|
|
92
138
|
const getJson = async (url) => {
|
|
@@ -224,8 +270,6 @@
|
|
|
224
270
|
}
|
|
225
271
|
}
|
|
226
272
|
|
|
227
|
-
const DATE = new Date().toISOString().split("T")[0];
|
|
228
|
-
|
|
229
273
|
const Logo = () => m`
|
|
230
274
|
<svg
|
|
231
275
|
width="256"
|
|
@@ -305,7 +349,7 @@
|
|
|
305
349
|
super(props);
|
|
306
350
|
this.state = {
|
|
307
351
|
numDays: 30,
|
|
308
|
-
date:
|
|
352
|
+
date: pacificToday(),
|
|
309
353
|
chartField: "users",
|
|
310
354
|
chartContext: "all",
|
|
311
355
|
uiSectionsLoaded: false,
|
|
@@ -315,25 +359,22 @@
|
|
|
315
359
|
async componentDidMount() {
|
|
316
360
|
await fetchUI();
|
|
317
361
|
this.setState({ uiSectionsLoaded: true });
|
|
318
|
-
|
|
362
|
+
// The chart waits for the stats so its field and context can be checked against them.
|
|
363
|
+
let stats = null;
|
|
364
|
+
await fetchData({
|
|
319
365
|
numDays: this.state.numDays,
|
|
320
366
|
date: this.state.date,
|
|
321
367
|
getDataFn,
|
|
322
|
-
onLoadComplete: (
|
|
368
|
+
onLoadComplete: (s) => (stats = s),
|
|
323
369
|
});
|
|
324
|
-
|
|
325
|
-
numDays: this.state.numDays,
|
|
326
|
-
date: this.state.date,
|
|
327
|
-
chartContext: this.state.chartContext,
|
|
370
|
+
const { chartField, chartContext } = resolveChartDefaults({
|
|
328
371
|
chartField: this.state.chartField,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
},
|
|
333
|
-
onLoadComplete: (data, max) => {
|
|
334
|
-
this.setState({ chartData: { loading: false, data, max } });
|
|
335
|
-
},
|
|
372
|
+
chartContext: this.state.chartContext,
|
|
373
|
+
stats,
|
|
374
|
+
contexts: UI.chartContexts,
|
|
336
375
|
});
|
|
376
|
+
this.setState({ stats, chartField, chartContext });
|
|
377
|
+
this.fetchChartData({ chartField, chartContext });
|
|
337
378
|
}
|
|
338
379
|
|
|
339
380
|
async fetchChartData({ numDays, date, chartContext, chartField }) {
|
package/package.json
CHANGED
package/src/public/js/App.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
getData,
|
|
8
8
|
fetchChartData,
|
|
9
9
|
fetchData,
|
|
10
|
+
resolveChartDefaults,
|
|
10
11
|
} from "./util.js";
|
|
11
12
|
import Tile from "./Tile.js";
|
|
12
|
-
|
|
13
|
-
const DATE = new Date().toISOString().split("T")[0];
|
|
13
|
+
import { pacificToday } from "../../shared/utils.js";
|
|
14
14
|
|
|
15
15
|
const Logo = () => html`
|
|
16
16
|
<svg
|
|
@@ -91,7 +91,7 @@ class App extends Component {
|
|
|
91
91
|
super(props);
|
|
92
92
|
this.state = {
|
|
93
93
|
numDays: 30,
|
|
94
|
-
date:
|
|
94
|
+
date: pacificToday(),
|
|
95
95
|
chartField: "users",
|
|
96
96
|
chartContext: "all",
|
|
97
97
|
uiSectionsLoaded: false,
|
|
@@ -101,25 +101,22 @@ class App extends Component {
|
|
|
101
101
|
async componentDidMount() {
|
|
102
102
|
await fetchUI();
|
|
103
103
|
this.setState({ uiSectionsLoaded: true });
|
|
104
|
-
|
|
104
|
+
// The chart waits for the stats so its field and context can be checked against them.
|
|
105
|
+
let stats = null;
|
|
106
|
+
await fetchData({
|
|
105
107
|
numDays: this.state.numDays,
|
|
106
108
|
date: this.state.date,
|
|
107
109
|
getDataFn,
|
|
108
|
-
onLoadComplete: (
|
|
110
|
+
onLoadComplete: (s) => (stats = s),
|
|
109
111
|
});
|
|
110
|
-
|
|
111
|
-
numDays: this.state.numDays,
|
|
112
|
-
date: this.state.date,
|
|
113
|
-
chartContext: this.state.chartContext,
|
|
112
|
+
const { chartField, chartContext } = resolveChartDefaults({
|
|
114
113
|
chartField: this.state.chartField,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
},
|
|
119
|
-
onLoadComplete: (data, max) => {
|
|
120
|
-
this.setState({ chartData: { loading: false, data, max } });
|
|
121
|
-
},
|
|
114
|
+
chartContext: this.state.chartContext,
|
|
115
|
+
stats,
|
|
116
|
+
contexts: UI.chartContexts,
|
|
122
117
|
});
|
|
118
|
+
this.setState({ stats, chartField, chartContext });
|
|
119
|
+
this.fetchChartData({ chartField, chartContext });
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
async fetchChartData({ numDays, date, chartContext, chartField }) {
|
package/src/public/js/util.js
CHANGED
|
@@ -72,6 +72,18 @@ export async function fetchData({
|
|
|
72
72
|
|
|
73
73
|
/* END methods for fetching data from the server */
|
|
74
74
|
|
|
75
|
+
// Keeps the preferred chart field/context when they exist, otherwise falls back to the first
|
|
76
|
+
// available one, so the menus (which show the first option when nothing matches) and the chart agree.
|
|
77
|
+
export function resolveChartDefaults({ chartField, chartContext, stats, contexts }) {
|
|
78
|
+
const fields = Object.keys(stats?.metrics?.all || {});
|
|
79
|
+
const contextIds = (contexts || []).map((c) => c.id);
|
|
80
|
+
return {
|
|
81
|
+
chartField: fields.length && !fields.includes(chartField) ? fields[0] : chartField,
|
|
82
|
+
chartContext:
|
|
83
|
+
contextIds.length && !contextIds.includes(chartContext) ? contextIds[0] : chartContext,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
/* START helper methods */
|
|
76
88
|
|
|
77
89
|
export const getJson = async (url) => {
|
package/src/shared/utils.js
CHANGED
|
@@ -65,3 +65,7 @@ export function defaultEndDate(now = nowAsPstDate()) {
|
|
|
65
65
|
yesterday.setUTCHours(0, 0, 0, 0);
|
|
66
66
|
return yesterday;
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// Today's calendar date in Pacific time as YYYY-MM-DD (daylight saving included), the same day
|
|
70
|
+
// buckets the server uses. The dashboard page defaults its date to this, not the browser's UTC date.
|
|
71
|
+
export const pacificToday = (now = new Date()) => iso(nowAsPstDate(now));
|