Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The mock serves Skip's full session/config surface (`loginStatus`, `applicationD
- **Specs run against the REAL app services, by design.** Under this runner the setup file's classes are *different module instances* than the app bundle's, so **any** `{ provide: AppClass, … }` in `src/test.ts` — a stub **or** the real class — is **DI-inert** (proven in #159). A component that injects a **`providedIn: 'root'`** service still gets its real root instance regardless; a **non-root** service (e.g. `UnitsService`, a plain `@Injectable()`) must be provided by the spec that needs it. So `src/test.ts` provides **only** what actually takes effect: framework tokens (`MAT_DIALOG_DATA`, `MatDialogRef`, `MatBottomSheetRef`, `ActivatedRoute`, `FormGroupDirective`, zoneless CD, `HttpClientTesting`, `NoopAnimations`), env/DOM shims (canvas, `ResizeObserver`, `matchMedia`, fonts, media), and icon registration. It provides **no** app services — not `SettingsService` / `AuthenticationService` / `SignalKConnectionService` / `ConnectionStateMachine` / `UnitsService` / the widget host directives.
- **A spec that needs a fake declares it locally.** Add a local provider in that spec's `configureTestingModule` (globals are prepended, so a local `{ provide: X, useValue: … }` wins). That is the whole mocking model — there is no shared app-service stub to extend. A local `SignalKConnectionService` fake must still expose `serverServiceEndpoint$` and `serverVersion$` (services subscribe to them at construction).
- **The failure mode is real-service construction, not stub drift.** Because components receive the real services, a service that throws at construction (missing dependency, a `requireSync`/NG0601 hazard) fails the spec directly — construction-time resilience of the real services is load-bearing for ~30 spec files. Fix the app code or add a local fake; do **not** re-add a global app-service stub (it is inert anyway).
- **Chart.js is a shim, aliased not mocked.** `vitest.config.ts` aliases `chart.js`, `chartjs-plugin-annotation`, `chartjs-adapter-date-fns` and `@aziham/chartjs-plugin-streaming` to `src/test-shims/chartjs-*`, the same way gridstack and canvas-gauges are aliased. **Do not add a per-file `vi.mock('chart.js')`** — that is what #544 was: a per-file mock only wins when its spec is the first in the worker to load the module, and `widget-numeric` reaches chart.js through `MinichartComponent` without mocking, so the real library was sometimes cached first. The real library cannot acquire a 2D context under jsdom, so `chart.ctx` comes out falsy and components silently drop everything they feed the chart — a live subscription plotting nothing, which reads as a flake rather than a wiring error. The shims are imported from `src/test.ts` so their chunks resolve during setup; a spec that first reaches one lazily can otherwise request it after its worker's environment is torn down (`EnvironmentTeardownError`, which fails the suite file while every test passes).
- **Chart.js is a shim, aliased not mocked.** `vitest.config.ts` aliases `chart.js`, `chartjs-plugin-annotation`, `chartjs-adapter-date-fns` and `@aziham/chartjs-plugin-streaming` to `src/test-shims/chartjs-*`, the same way gridstack and canvas-gauges are aliased. **Do not add a per-file `vi.mock('chart.js')`** — that is what #544 was: a per-file mock only wins when its spec is the first in the worker to load the module, and `widget-numeric` reaches chart.js through `MinigraphComponent` without mocking, so the real library was sometimes cached first. The real library cannot acquire a 2D context under jsdom, so `chart.ctx` comes out falsy and components silently drop everything they feed the chart — a live subscription plotting nothing, which reads as a flake rather than a wiring error. The shims are imported from `src/test.ts` so their chunks resolve during setup; a spec that first reaches one lazily can otherwise request it after its worker's environment is torn down (`EnvironmentTeardownError`, which fails the suite file while every test passes).
- **The gauge library is a no-op shim in tests.** `vitest.config.ts` aliases `@godind/ng-canvas-gauges` to `src/test-shims/ng-canvas-gauges-shim.ts`, whose `LinearGauge`/`RadialGauge` expose only the `options` and `value` inputs — **there is no `update()`**. Every `this.ngGauge()?.update(...)` in the gauge widgets therefore throws `update is not a function` and is swallowed by the surrounding `catch`, so anything asserted only through those imperative pushes is unobservable: a misspelled option key or a null option value passes CI. Test the *decision* instead — call `buildGaugeOptions()` and assert `gaugeOptions`, or extract the rule into a method and assert that. Real library behaviour needs a browser (see `perf-harness/`).
- **Local runs work.** The whole suite runs locally via `npm test` (~90s, build-dominated), and a single spec via `ng test --include='<path/to/file.spec.ts>'`; plain `npx vitest run <file>` fails (`@angular/compiler is not available`) because it bypasses the builder. **CI on Node 24 is the authoritative gate** (`npm run ci` = `lint` + `snc` + `test:headless` + `test:mcp-schema`).
- CI uses **`npm ci`** (the `run-tests` action and `release.yml`) against a lockfile that is in sync. If `npm ci` ever fails on missing optional platform deps, regenerate the lockfile (`rm package-lock.json && npm install`) and commit it — don't switch CI back to `npm install`.
Expand All @@ -85,7 +85,7 @@ The mock serves Skip's full session/config surface (`loginStatus`, `applicationD

A session-less visitor is a third shape, not an error: when the server reports `readOnlyAccess` (its `allow_readonly`) and does not ask for OIDC auto-login, the bootstrap returns `anonymous` instead of redirecting, and loads the shared `global`-scope slot named `default` — the only config an anonymous principal can read — falling back to `buildDefaultConfig()`. Nothing in Skip writes that slot; an operator publishes it out of band. **Write capability has two gates and `StorageService.canPersist()` is both of them**: the session's `userLevel` must allow writes, AND the loaded config must be the session's own rather than a shared read-only view (`isReadOnlyContext()`, set by the bootstrap). The second gate is what stops an anonymous tab that later acquires a session from writing the shared config into a real user's slot. `DashboardService.isReadOnlySession` is the signal every affordance and guard reads, so a lock and its button cannot disagree.

**History & charts**: the SK **v2 History API** is consumed by `HistoryApiClientService`; `HistoryToChartMapperService` adapts history values to chart datapoints; `DashboardHistorySeriesSyncService` resolves per-widget history series for the history dialog; `HistoryChartStreamService` feeds the trend-chart widgets (History-API backfill plus a thin delta-stream live tail). Skip ships no server-side history provider — the History API is served by an external provider (InfluxDB via `signalk-to-influxdb2`, or `signalk-parquet`), and charts render an empty state when none is present.
**History & graphs**: time-series visualizations are **graphs** throughout the UI and the code — "chart" is reserved for the nautical sense and for chart.js's own API. The SK **v2 History API** is consumed by `HistoryApiClientService`; `HistoryToGraphMapperService` adapts history values to graph datapoints; `DashboardHistorySeriesSyncService` resolves per-widget history series for the history dialog; `HistoryGraphStreamService` feeds the trend-graph widgets (History-API backfill plus a thin delta-stream live tail). Skip ships no server-side history provider — the History API is served by an external provider (InfluxDB via `signalk-to-influxdb2`, or `signalk-parquet`), and graphs render an empty state when none is present. Two widget selectors (`widget-data-chart`, `widget-windtrends-chart`) keep the old spelling because they are the persisted widget `type` in stored dashboards; renaming them needs a config migration (#592).

## Gotchas

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Skip is designed for sailors and boaters who want:
- A **modern, polished interface** optimized for marine displays.
- **Touch-optimized design**: touch-first, intuitive design for tablets, phones, and other touch-enabled devices.
- **Cross-platform support**: runs on phones, tablets, laptops, Raspberry Pi, Web Enabled TV or other fixed displays - anywhere you can run a web browser.
- **Instant access to all Signal K data**: displays gauges, plots, switches, and other widgets right out of the box.
- **Instant access to all Signal K data**: displays gauges, graphs, switches, and other widgets right out of the box.
- **Flexible dashboards**: customize layouts, drag-and-drop widgets, night/day mode, kiosk/fullscreen and remote control support.

With Skip, you get the **clarity of a purpose-built marine instrument panel** combined with the flexibility of Signal K. It’s simple, reliable, and highly usable — a modern, touch-first instrument panel for [Signal K](https://signalk.org) vessels.
Expand Down Expand Up @@ -130,7 +130,7 @@ All Skip widgets are visual presentation controls that are very versatile, with
- **AC/DC Charger**- Monitor charging performance at a glance with a compact AC/DC Charger Widget. View single or multiple chargers with charge mode, voltage, current, power and temperature. Chargers are discovered automatically.
- **Freeboard-SK** – Adds the Freeboard-SK chart plotter as a widget with automatic sign-in.
- **Autopilot Head** – Typical autopilot controls for compatible Signal K Autopilot devices.
- **Realtime Data Plot** – Visualizes data on a real-time plot with actuals, averages, and min/max.
- **Data Graph** – Graphs any numeric path over a configurable time window, with actuals, averages, and min/max.
- **AIS Radar**: Display AIS targets with range rings, interactive target details, and quick zoom and filtering controls.
- **Embed Webpage Viewer** – Embeds external web apps (Grafana, Node-RED, etc.) into your dashboard.
- **Racesteer** – Race steering display fusing polar performance data with live conditions for optimal tactics.
Expand All @@ -154,7 +154,7 @@ Grafana integration with other widgets
![Embedded Webpage Concept Image](./images/SkipGaugeSample3-1024x508.png)

## Historical Data
Skip plots recent history for your numeric data by reading it from an external Signal K History API provider (such as `signalk-to-influxdb2` or `signalk-parquet`). Press and hold (long-press) a widget to open its history dialog, or use a Realtime Data Plot or Wind Trends widget to see recent trends. Skip does **not** record or store data itself — the detail and time span available depend on whatever provider your Signal K server runs, and plots show live data only when no provider is present. See the [History-API Provider](src/assets/help-docs/history-api.md) help file for setup.
Skip graphs recent history for your numeric data by reading it from an external Signal K History API provider (such as `signalk-to-influxdb2` or `signalk-parquet`). Press and hold (long-press) a widget to open its history dialog, or use a Data Graph or Wind Trends widget to see recent trends. Skip does **not** record or store data itself — the detail and time span available depend on whatever provider your Signal K server runs, and without a provider both the history dialog and the graph widgets show an empty state. See the [History-API Provider](src/assets/help-docs/history-api.md) help file for setup.

## Night Modes
Keep your night vision with automatic or manual day and night switching to a color preserving dim mode or an all Red theme. The images below look very dark, but at night... they are perfect!
Expand Down Expand Up @@ -187,7 +187,7 @@ Typical complementary components you may install (most are often bundled with Si
- **Node‑RED** – Low‑code, flow‑based wiring of devices, APIs, online services, and custom logic (alert escalation, device control automation, data enrichment, protocol bridging).

**Data Storage & Analytics**
- **InfluxDB / other TSDB** – High‑resolution historical storage of sensor & performance metrics beyond what lightweight widget plots should retain.
- **InfluxDB / other TSDB** – High‑resolution historical storage of sensor & performance metrics beyond what lightweight widget graphs should retain.
- **Grafana** – Rich exploratory / comparative dashboards, ad‑hoc queries, alert rules on stored metrics, correlation across heterogeneous data sources.

## Harness the Power of Data State Notifications
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ registerChartComponents();

/**
* Connection status and diagnostics: server session identity (SSO), server/stream state, versions,
* internet reachability, and a live delta-throughput chart. Read-only — the connection itself is
* internet reachability, and a live delta-throughput graph. Read-only — the connection itself is
* auto-configured (same-origin, server-discovered endpoints), so there is nothing to edit here.
* Skip authenticates only through the server's same-origin session (SSO); it has no credential
* entry of its own.
Expand Down Expand Up @@ -82,16 +82,16 @@ export class ConnectionStatusComponent implements AfterViewInit, OnDestroy {
});

private _chart: Chart | null = null;
private textColor: string; // Store the computed text color for chart styling
private textColor: string; // Store the computed text color for graph styling

ngAfterViewInit(): void {
const canvas = this.activityGraph()?.nativeElement;
if (!canvas) return;
this.textColor = window.getComputedStyle(canvas).color;
this._chart?.destroy();
this.startChart(canvas);
this.startGraph(canvas);

// Get real-time WebSocket Delta update statistics for chart
// Get real-time WebSocket Delta update statistics for graph
this.DataService.getSignalkDeltaUpdateStatistics().pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((update: IDeltaUpdate) => {
Expand All @@ -107,9 +107,9 @@ export class ConnectionStatusComponent implements AfterViewInit, OnDestroy {

/**
* Initializes the Chart.js line chart for displaying WebSocket delta statistics.
* Creates a time-series chart showing data update frequency over time.
* Creates a time-series graph showing data update frequency over time.
*/
private startChart(canvas: HTMLCanvasElement) {
private startGraph(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
this._chart = new Chart(ctx, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ <h5 mat-dialog-title>{{data.title}}</h5>
</div>
<mat-dialog-content class="history-dialog-content">
<div class="time-select">
<mat-button-toggle-group [(ngModel)]="selectedPeriod" hideSingleSelectionIndicator="true" aria-label="Plot Time Span Selection">
<mat-button-toggle-group [(ngModel)]="selectedPeriod" hideSingleSelectionIndicator="true" aria-label="Graph Time Span Selection">
<mat-button-toggle value="PT15M" (click)="setPeriod('PT15M')">15 Minutes</mat-button-toggle>
<mat-button-toggle value="PT1H" (click)="setPeriod('PT1H')">1 Hour</mat-button-toggle>
<mat-button-toggle value="PT8H" (click)="setPeriod('PT8H')">8 Hours</mat-button-toggle>
<mat-button-toggle value="PT24H" (click)="setPeriod('PT24H')">24 Hours</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div class="chart-area">
<div class="graph-area">
@if (loading()) {
<p class="history-message">Loading historical data…</p>
} @else if (error()) {
<p class="history-message error">503: {{ error() }}</p>
} @else if (hasNoData()) {
<p class="history-message">No historical data is available for this widget. Historical data comes from an external Signal K history provider (such as signalk-to-influxdb2 or signalk-parquet) — make sure one is installed and configured to support the widget's path, then try again.</p>
} @else {
<div class="chart-wrapper">
<div class="graph-wrapper">
<canvas #historyCanvas></canvas>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ mat-dialog-content.history-dialog-content {
width: 100%;
}

.chart-area {
.graph-area {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
width: 100%;
}

.chart-wrapper {
.graph-wrapper {
display: flex;
flex-direction: column;
flex: 1 1 0;
Expand Down
Loading
Loading