@meshmakers/octo-meshboard 3.4.1130 → 3.4.1160
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.
|
@@ -2535,17 +2535,26 @@ class MeshBoardStateService {
|
|
|
2535
2535
|
/**
|
|
2536
2536
|
* The IANA time zone to forward to the resolution-aware series query so calendar
|
|
2537
2537
|
* rollups are selected in the board's zone (AB#4190). Maps the board mode:
|
|
2538
|
-
* `'utc'` → `'UTC'`, an IANA id → itself, and `'local'` →
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2538
|
+
* `'utc'` → `'UTC'`, an IANA id → itself, and `'local'` → the browser's own IANA
|
|
2539
|
+
* zone. The result is therefore always the same basis `resolveCurrentTimeRange()`
|
|
2540
|
+
* used to compute the `{from, to}` window — in `'local'` mode that window is built
|
|
2541
|
+
* from browser-local wall clocks, so naming that zone is what makes the two agree.
|
|
2542
|
+
*
|
|
2543
|
+
* `'local'` used to send nothing, which left the resolver on UTC. Under the default
|
|
2544
|
+
* per-query comparison policy a calendar rung stored in another zone then has no
|
|
2545
|
+
* determinate grain and drops out of the ladder, so the board silently fell back to
|
|
2546
|
+
* a finer rung, reported signal `Ok`, and drew a window that was not the one asked
|
|
2547
|
+
* for (AB#5157 review). `undefined` now only means the browser named no zone at all.
|
|
2542
2548
|
*/
|
|
2543
2549
|
resolveStreamDataTimeZone() {
|
|
2544
2550
|
const mode = this.timeZoneMode();
|
|
2551
|
+
if (mode === 'utc') {
|
|
2552
|
+
return 'UTC';
|
|
2553
|
+
}
|
|
2545
2554
|
if (mode === 'local') {
|
|
2546
|
-
return undefined;
|
|
2555
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
|
|
2547
2556
|
}
|
|
2548
|
-
return mode
|
|
2557
|
+
return mode;
|
|
2549
2558
|
}
|
|
2550
2559
|
/**
|
|
2551
2560
|
* Stores the source rtIds resolved from an entity selector's current
|
|
@@ -2945,6 +2954,7 @@ const ResolveSeriesQueryDocumentDto = gql `
|
|
|
2945
2954
|
signal
|
|
2946
2955
|
actualPoints
|
|
2947
2956
|
diagnostic
|
|
2957
|
+
finerRungAvailableFrom
|
|
2948
2958
|
}
|
|
2949
2959
|
}
|
|
2950
2960
|
}
|
|
@@ -3158,7 +3168,8 @@ class QueryExecutorService {
|
|
|
3158
3168
|
reducingFunction: decision.reducingFunction,
|
|
3159
3169
|
signal: decision.signal,
|
|
3160
3170
|
actualPoints: decision.actualPoints ?? null,
|
|
3161
|
-
diagnostic: decision.diagnostic ?? null
|
|
3171
|
+
diagnostic: decision.diagnostic ?? null,
|
|
3172
|
+
finerRungAvailableFrom: decision.finerRungAvailableFrom ?? null
|
|
3162
3173
|
};
|
|
3163
3174
|
}
|
|
3164
3175
|
/**
|
|
@@ -14626,6 +14637,9 @@ class LineChartWidgetComponent {
|
|
|
14626
14637
|
* Resolution-aware hint (AB#4290): a short badge describing the archive-selection outcome —
|
|
14627
14638
|
* `null` for a clean reduction (signal OK), a warning otherwise (fewer points delivered, or no
|
|
14628
14639
|
* compatible rollup so the raw archive was returned unreduced). Mirrors the resolver's signal.
|
|
14640
|
+
* `CoverageLimited` (AB#5157) is a distinct badge: a finer rung was skipped for holding no data
|
|
14641
|
+
* over the window, which is a history gap rather than a density problem — so it is always shown,
|
|
14642
|
+
* bypassing the {@link RESOLUTION_LIMIT_WARN_PX} suppression that mutes a merely coarse line.
|
|
14629
14643
|
*/
|
|
14630
14644
|
resolutionHint = computed(() => {
|
|
14631
14645
|
const s = this._resolutionSignal();
|
|
@@ -14633,6 +14647,20 @@ class LineChartWidgetComponent {
|
|
|
14633
14647
|
return null;
|
|
14634
14648
|
const diag = s.diagnostic ?? '';
|
|
14635
14649
|
switch (s.signal) {
|
|
14650
|
+
case SeriesResolutionSignalDto.CoverageLimitedDto: {
|
|
14651
|
+
// Coverage, not density (AB#5157): the finer rung was skipped because it holds NO data
|
|
14652
|
+
// over the window, so the pixel-density suppression must not apply — a dense-looking
|
|
14653
|
+
// coarse line is still the wrong answer to "where is my history?". Always flagged.
|
|
14654
|
+
const availableFrom = formatInstant(s.finerRungAvailableFrom, this.stateService.timeZoneMode(), {
|
|
14655
|
+
day: '2-digit', month: '2-digit', year: 'numeric'
|
|
14656
|
+
});
|
|
14657
|
+
return {
|
|
14658
|
+
text: '⚠ no history',
|
|
14659
|
+
title: availableFrom
|
|
14660
|
+
? `History at this resolution is available from ${availableFrom}`
|
|
14661
|
+
: diag || 'A finer resolution exists but holds no data for this time range.'
|
|
14662
|
+
};
|
|
14663
|
+
}
|
|
14636
14664
|
case SeriesResolutionSignalDto.ResolutionLimitedDto: {
|
|
14637
14665
|
const delivered = s.actualPoints ?? s.points;
|
|
14638
14666
|
const requested = this._resolutionTarget();
|
|
@@ -14667,7 +14695,20 @@ class LineChartWidgetComponent {
|
|
|
14667
14695
|
const delivered = s.actualPoints ?? s.points;
|
|
14668
14696
|
const requested = this._resolutionTarget();
|
|
14669
14697
|
const ofReq = requested ? ` (the chart could show ~${requested})` : '';
|
|
14698
|
+
const diag = s.diagnostic ?? '';
|
|
14670
14699
|
switch (s.signal) {
|
|
14700
|
+
case SeriesResolutionSignalDto.CoverageLimitedDto: {
|
|
14701
|
+
const availableFrom = formatInstant(s.finerRungAvailableFrom, this.stateService.timeZoneMode(), {
|
|
14702
|
+
day: '2-digit', month: '2-digit', year: 'numeric'
|
|
14703
|
+
});
|
|
14704
|
+
const from = availableFrom
|
|
14705
|
+
? ` That resolution only has history from ${availableFrom} onwards.`
|
|
14706
|
+
: '';
|
|
14707
|
+
return `A finer resolution exists for this series, but it holds no data for the selected time range, `
|
|
14708
|
+
+ `so the chart fell back to a coarser one (${delivered} point(s)${ofReq}).${from} `
|
|
14709
|
+
+ `The values are correct — narrow the range to a covered period to see finer detail.`
|
|
14710
|
+
+ (diag ? ` ${diag}` : '');
|
|
14711
|
+
}
|
|
14671
14712
|
case SeriesResolutionSignalDto.ResolutionLimitedDto:
|
|
14672
14713
|
return `This chart auto-selects the coarsest stored resolution that still fits the view. `
|
|
14673
14714
|
+ `For this time range the finest matching rollup only provides ${delivered} point(s)${ofReq}, `
|
|
@@ -14925,28 +14966,23 @@ class LineChartWidgetComponent {
|
|
|
14925
14966
|
if (!resolution) {
|
|
14926
14967
|
return { rows: [], signal: null, requestedPoints: targetPoints };
|
|
14927
14968
|
}
|
|
14928
|
-
//
|
|
14929
|
-
//
|
|
14930
|
-
//
|
|
14931
|
-
//
|
|
14932
|
-
//
|
|
14933
|
-
//
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
const bucket = resolution.effectiveBucketMs;
|
|
14938
|
-
const alignedFrom = Math.floor(from.getTime() / bucket) * bucket;
|
|
14939
|
-
qFrom = new Date(alignedFrom);
|
|
14940
|
-
qTo = new Date(alignedFrom + Math.max(1, resolution.points) * bucket);
|
|
14941
|
-
}
|
|
14969
|
+
// The requested window is passed through unchanged. The widget used to snap it to the
|
|
14970
|
+
// resolver's effective-bucket grid, because the downsampling engine binned from whatever start
|
|
14971
|
+
// it was handed and an unaligned window (a relative "last N days" filter ending at the current
|
|
14972
|
+
// wall clock) made every bin miss the stored points. That alignment now happens where the grain
|
|
14973
|
+
// is actually known — the engine snaps its own axis origin — and doing it here was wrong twice
|
|
14974
|
+
// over for a calendar rung (AB#5157 review): `effectiveBucketMs` is an average, so anchoring it
|
|
14975
|
+
// to the epoch landed the axis off the calendar boundaries and outside the requested window,
|
|
14976
|
+
// and sizing the end as points × bucket cut the running period off. No client can compute this
|
|
14977
|
+
// axis: neither the grain nor the rung's alignment is part of the query contract.
|
|
14942
14978
|
// Group-aggregation mode (AB#4714): one downsampling call PER GROUP, each with the whole
|
|
14943
14979
|
// group's rtIds, so the transient downsampling reduces over every member (per
|
|
14944
14980
|
// requiredAggregation) into one aggregate line. Default mode: one call per source rtId, keeping
|
|
14945
14981
|
// each source its own series. See loadGroupAggregatedRows / loadPerRtIdRows.
|
|
14946
14982
|
const downsample = (rtIds, label) => this.queryExecutor.downsampleByArchive({
|
|
14947
14983
|
archiveRtId: resolution.archiveRtId,
|
|
14948
|
-
from
|
|
14949
|
-
to
|
|
14984
|
+
from,
|
|
14985
|
+
to,
|
|
14950
14986
|
limit: Math.max(1, resolution.points),
|
|
14951
14987
|
sourcePath,
|
|
14952
14988
|
aggregation: resolution.reducingFunction,
|
|
@@ -15731,7 +15767,7 @@ class LineChartConfigDialogComponent {
|
|
|
15731
15767
|
[(ngModel)]="selectedPersistentQuery"
|
|
15732
15768
|
(querySelected)="onQuerySelected($event)"
|
|
15733
15769
|
placeholder="Select a Query..."
|
|
15734
|
-
hint="Select a query
|
|
15770
|
+
hint="Select a stream-data query on the archive you want to chart — a simple one is enough. The widget picks the rollup rung and downsamples itself; the fields below group the rows into series.">
|
|
15735
15771
|
</mm-query-selector>
|
|
15736
15772
|
</div>
|
|
15737
15773
|
|
|
@@ -15992,7 +16028,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
15992
16028
|
[(ngModel)]="selectedPersistentQuery"
|
|
15993
16029
|
(querySelected)="onQuerySelected($event)"
|
|
15994
16030
|
placeholder="Select a Query..."
|
|
15995
|
-
hint="Select a query
|
|
16031
|
+
hint="Select a stream-data query on the archive you want to chart — a simple one is enough. The widget picks the rollup rung and downsamples itself; the fields below group the rows into series.">
|
|
15996
16032
|
</mm-query-selector>
|
|
15997
16033
|
</div>
|
|
15998
16034
|
|