@meshmakers/octo-meshboard 3.4.1120 → 3.4.1140

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.
@@ -1110,9 +1110,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1110
1110
  }]
1111
1111
  }], ctorParameters: () => [{ type: i1.Apollo }] });
1112
1112
 
1113
- // Re-export all GraphQL types from @meshmakers/octo-services
1114
- // This file is NOT generated - it imports shared types from the library
1115
-
1116
1113
  /**
1117
1114
  * Central registry for widget types.
1118
1115
  * Manages widget components, config dialogs, and their interactions.
@@ -2538,17 +2535,26 @@ class MeshBoardStateService {
2538
2535
  /**
2539
2536
  * The IANA time zone to forward to the resolution-aware series query so calendar
2540
2537
  * rollups are selected in the board's zone (AB#4190). Maps the board mode:
2541
- * `'utc'` → `'UTC'`, an IANA id → itself, and `'local'` → `undefined` (the browser's
2542
- * own zone has no server-side meaning, so the resolver stays on UTC — the same
2543
- * basis its calendar rungs use by default). The result is consistent with the zone
2544
- * `resolveCurrentTimeRange()` used to compute the `{from, to}` window.
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.
2545
2548
  */
2546
2549
  resolveStreamDataTimeZone() {
2547
2550
  const mode = this.timeZoneMode();
2551
+ if (mode === 'utc') {
2552
+ return 'UTC';
2553
+ }
2548
2554
  if (mode === 'local') {
2549
- return undefined;
2555
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
2550
2556
  }
2551
- return mode === 'utc' ? 'UTC' : mode;
2557
+ return mode;
2552
2558
  }
2553
2559
  /**
2554
2560
  * Stores the source rtIds resolved from an entity selector's current
@@ -2948,6 +2954,7 @@ const ResolveSeriesQueryDocumentDto = gql `
2948
2954
  signal
2949
2955
  actualPoints
2950
2956
  diagnostic
2957
+ finerRungAvailableFrom
2951
2958
  }
2952
2959
  }
2953
2960
  }
@@ -3161,7 +3168,8 @@ class QueryExecutorService {
3161
3168
  reducingFunction: decision.reducingFunction,
3162
3169
  signal: decision.signal,
3163
3170
  actualPoints: decision.actualPoints ?? null,
3164
- diagnostic: decision.diagnostic ?? null
3171
+ diagnostic: decision.diagnostic ?? null,
3172
+ finerRungAvailableFrom: decision.finerRungAvailableFrom ?? null
3165
3173
  };
3166
3174
  }
3167
3175
  /**
@@ -9366,7 +9374,7 @@ class TableWidgetComponent {
9366
9374
  </div>
9367
9375
  }
9368
9376
  </div>
9369
- `, isInline: true, styles: [".table-widget{height:100%;width:100%;display:flex;flex-direction:column}.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;color:var(--kendo-color-subtle, #6c757d);font-style:italic}:host ::ng-deep mm-list-view{height:100%}:host ::ng-deep .k-grid{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ListViewComponent, selector: "mm-list-view", inputs: ["pageSize", "skip", "autoPageSize", "rowIsClickable", "showRowCheckBoxes", "showRowSelectAllCheckBox", "contextMenuType", "leftToolbarActions", "rightToolbarActions", "actionCommandItems", "contextMenuCommandItems", "excelExportFileName", "pdfExportFileName", "pageable", "sortable", "rowFilterEnabled", "searchTextBoxEnabled", "persistListState", "listStateKey", "resizable", "actionsColumnWidth", "hideCheckboxesBelow", "cardModeBelow", "rowClass", "messages", "selectable", "columns"], outputs: ["rowClicked", "resetFilters"] }, { kind: "directive", type: TableWidgetDataSourceDirective, selector: "[mmTableWidgetDataSource]", inputs: ["config"], outputs: ["queryColumnsLoaded"], exportAs: ["mmTableWidgetDataSource"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
9377
+ `, isInline: true, styles: [".table-widget{height:100%;width:100%;display:flex;flex-direction:column}.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;color:var(--kendo-color-subtle, #6c757d);font-style:italic}:host ::ng-deep mm-list-view{height:100%}:host ::ng-deep .k-grid{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ListViewComponent, selector: "mm-list-view", inputs: ["pageSize", "skip", "autoPageSize", "rowIsClickable", "showRowCheckBoxes", "showRowSelectAllCheckBox", "contextMenuType", "leftToolbarActions", "rightToolbarActions", "actionCommandItems", "contextMenuCommandItems", "excelExportFileName", "pdfExportFileName", "pageable", "sortable", "rowFilterEnabled", "searchTextBoxEnabled", "persistListState", "listStateKey", "resizable", "actionsColumnWidth", "hideCheckboxesBelow", "cardModeBelow", "collapseCommandsBelow", "rowClass", "messages", "selectable", "columns"], outputs: ["rowClicked", "resetFilters"] }, { kind: "directive", type: TableWidgetDataSourceDirective, selector: "[mmTableWidgetDataSource]", inputs: ["config"], outputs: ["queryColumnsLoaded"], exportAs: ["mmTableWidgetDataSource"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
9370
9378
  }
9371
9379
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: TableWidgetComponent, decorators: [{
9372
9380
  type: Component,
@@ -14629,6 +14637,9 @@ class LineChartWidgetComponent {
14629
14637
  * Resolution-aware hint (AB#4290): a short badge describing the archive-selection outcome —
14630
14638
  * `null` for a clean reduction (signal OK), a warning otherwise (fewer points delivered, or no
14631
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.
14632
14643
  */
14633
14644
  resolutionHint = computed(() => {
14634
14645
  const s = this._resolutionSignal();
@@ -14636,6 +14647,20 @@ class LineChartWidgetComponent {
14636
14647
  return null;
14637
14648
  const diag = s.diagnostic ?? '';
14638
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
+ }
14639
14664
  case SeriesResolutionSignalDto.ResolutionLimitedDto: {
14640
14665
  const delivered = s.actualPoints ?? s.points;
14641
14666
  const requested = this._resolutionTarget();
@@ -14670,7 +14695,20 @@ class LineChartWidgetComponent {
14670
14695
  const delivered = s.actualPoints ?? s.points;
14671
14696
  const requested = this._resolutionTarget();
14672
14697
  const ofReq = requested ? ` (the chart could show ~${requested})` : '';
14698
+ const diag = s.diagnostic ?? '';
14673
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
+ }
14674
14712
  case SeriesResolutionSignalDto.ResolutionLimitedDto:
14675
14713
  return `This chart auto-selects the coarsest stored resolution that still fits the view. `
14676
14714
  + `For this time range the finest matching rollup only provides ${delivered} point(s)${ofReq}, `
@@ -14928,28 +14966,23 @@ class LineChartWidgetComponent {
14928
14966
  if (!resolution) {
14929
14967
  return { rows: [], signal: null, requestedPoints: targetPoints };
14930
14968
  }
14931
- // Snap the query window to the resolver's effective-bucket grid. The downsampling engine only
14932
- // aggregates a stored point into a bin when the bin width equals the data grain AND the window
14933
- // start sits on that grid (from the epoch). An unaligned window — e.g. a relative "last N days"
14934
- // filter ending at the current wall-clock time (…:42:54) — otherwise makes every bin miss the
14935
- // hourly/interval data and yields an all-null (blank) chart. Aligning from + sizing to
14936
- // points × bucket makes each bin land exactly on a stored point.
14937
- let qFrom = from;
14938
- let qTo = to;
14939
- if (resolution.effectiveBucketMs > 0) {
14940
- const bucket = resolution.effectiveBucketMs;
14941
- const alignedFrom = Math.floor(from.getTime() / bucket) * bucket;
14942
- qFrom = new Date(alignedFrom);
14943
- qTo = new Date(alignedFrom + Math.max(1, resolution.points) * bucket);
14944
- }
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.
14945
14978
  // Group-aggregation mode (AB#4714): one downsampling call PER GROUP, each with the whole
14946
14979
  // group's rtIds, so the transient downsampling reduces over every member (per
14947
14980
  // requiredAggregation) into one aggregate line. Default mode: one call per source rtId, keeping
14948
14981
  // each source its own series. See loadGroupAggregatedRows / loadPerRtIdRows.
14949
14982
  const downsample = (rtIds, label) => this.queryExecutor.downsampleByArchive({
14950
14983
  archiveRtId: resolution.archiveRtId,
14951
- from: qFrom,
14952
- to: qTo,
14984
+ from,
14985
+ to,
14953
14986
  limit: Math.max(1, resolution.points),
14954
14987
  sourcePath,
14955
14988
  aggregation: resolution.reducingFunction,
@@ -15734,7 +15767,7 @@ class LineChartConfigDialogComponent {
15734
15767
  [(ngModel)]="selectedPersistentQuery"
15735
15768
  (querySelected)="onQuerySelected($event)"
15736
15769
  placeholder="Select a Query..."
15737
- hint="Select a query that returns time-series data with grouping.">
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.">
15738
15771
  </mm-query-selector>
15739
15772
  </div>
15740
15773
 
@@ -15995,7 +16028,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
15995
16028
  [(ngModel)]="selectedPersistentQuery"
15996
16029
  (querySelected)="onQuerySelected($event)"
15997
16030
  placeholder="Select a Query..."
15998
- hint="Select a query that returns time-series data with grouping.">
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.">
15999
16032
  </mm-query-selector>
16000
16033
  </div>
16001
16034