Full Code of SignalK/signalk-server for AI

master d602bd648e17 cached
687 files
9.1 MB
2.4M tokens
2127 symbols
1 requests
Download .txt
Showing preview only (9,633K chars total). Download the full file or copy to clipboard to get everything.
Repository: SignalK/signalk-server
Branch: master
Commit: d602bd648e17
Files: 687
Total size: 9.1 MB

Directory structure:
gitextract_s_vrwrw7/

├── .coderabbit.yaml
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-base-image.yml
│       ├── build-docker.yml
│       ├── plugin-ci.yml
│       ├── release.yml
│       ├── require_pr_label.yml
│       ├── security-scan.yml
│       └── test.yml
├── .gitignore
├── .mocharc.js
├── .npmignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc.json
├── .python-version
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE
├── Procfile
├── README.md
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile_base_24.04
│   ├── Dockerfile_base_alpine
│   ├── Dockerfile_rel
│   ├── README.md
│   ├── avahi/
│   │   └── avahi-dbus.conf
│   ├── bluez/
│   │   └── bluezuser.conf
│   ├── docker-compose.yml
│   ├── startup.sh
│   └── v2_demo/
│       ├── Dockerfile
│       ├── course-data.json
│       ├── resources/
│       │   ├── routes/
│       │   │   ├── ad825f6c-1ae9-4f76-abc4-df2866b14b78
│       │   │   └── da825f6c-1ae9-4f76-abc4-df2866b14b78
│       │   └── waypoints/
│       │       ├── ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a
│       │       └── afe46290-aa98-4d2f-9c04-d199ca64942e
│       ├── resources-provider.json
│       ├── serverstate/
│       │   └── course/
│       │       └── settings.json
│       └── startup_heroku_demo.sh
├── docs/
│   ├── README.md
│   ├── breaking_changes.md
│   ├── develop/
│   │   ├── README.md
│   │   ├── plugins/
│   │   │   ├── README.md
│   │   │   ├── autopilot_provider_plugins.md
│   │   │   ├── backpressure.md
│   │   │   ├── ci.md
│   │   │   ├── configuration.md
│   │   │   ├── course_calculations.md
│   │   │   ├── custom_renderers.md
│   │   │   ├── deltas.md
│   │   │   ├── examples/
│   │   │   │   ├── plugin-caller-example.yml
│   │   │   │   ├── plugin-dependabot-example.yml
│   │   │   │   └── plugin-release-example.yml
│   │   │   ├── publishing.md
│   │   │   ├── release.md
│   │   │   ├── resource_provider_plugins.md
│   │   │   ├── wasm/
│   │   │   │   ├── README.md
│   │   │   │   ├── assemblyscript.md
│   │   │   │   ├── best_practices.md
│   │   │   │   ├── capabilities.md
│   │   │   │   ├── deltas.md
│   │   │   │   ├── go.md
│   │   │   │   ├── http_endpoints.md
│   │   │   │   ├── integration_guide.md
│   │   │   │   └── rust.md
│   │   │   └── weather_provider_plugins.md
│   │   ├── rest-api/
│   │   │   ├── README.md
│   │   │   ├── autopilot_api.md
│   │   │   ├── conventions.md
│   │   │   ├── course_api.md
│   │   │   ├── history_api.md
│   │   │   ├── notifications_api.md
│   │   │   ├── plugin_api.md
│   │   │   ├── proposed/
│   │   │   │   ├── README.md
│   │   │   │   └── anchor_api.md
│   │   │   ├── radar_api.md
│   │   │   ├── resources_api.md
│   │   │   └── weather_api.md
│   │   └── webapps.md
│   ├── guides/
│   │   ├── README.md
│   │   ├── anchoralarm/
│   │   │   └── anchoralarm.md
│   │   ├── datalogging/
│   │   │   └── datalogging.md
│   │   ├── navdataserver/
│   │   │   └── navdataserver.md
│   │   ├── udev.md
│   │   └── unitpreferences.md
│   ├── img/
│   │   ├── autopilot_provider.dia
│   │   ├── course_provider.dia
│   │   ├── notification_manager.dia
│   │   ├── resource_provider.dia
│   │   └── server_only.dia
│   ├── installation/
│   │   ├── README.md
│   │   ├── command_line.md
│   │   ├── docker.md
│   │   ├── npm.md
│   │   ├── raspberry_pi_installation.md
│   │   ├── source.md
│   │   └── updating.md
│   ├── internal/
│   │   ├── README.md
│   │   ├── wasm-architecture.md
│   │   └── wasm-asyncify.md
│   ├── oidc.md
│   ├── security-architecture.md
│   ├── security.md
│   ├── setup/
│   │   ├── configuration.md
│   │   ├── generating_tokens.md
│   │   ├── nmea.md
│   │   └── seatalk/
│   │       └── README.md
│   ├── src/
│   │   └── features/
│   │       └── weather/
│   │           └── weather.md
│   ├── support/
│   │   ├── help.md
│   │   └── sponsor.md
│   └── whats_new.md
├── empty_file
├── eslint.config.js
├── examples/
│   └── wasm-plugins/
│       ├── example-anchor-watch-rust/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── Cargo.toml
│       │   ├── README.md
│       │   ├── package.json
│       │   ├── src/
│       │   │   └── lib.rs
│       │   └── wit/
│       │       └── signalk-plugin.wit
│       ├── example-hello-assemblyscript/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── README.md
│       │   ├── asconfig.json
│       │   ├── assembly/
│       │   │   └── index.ts
│       │   └── package.json
│       ├── example-routes-waypoints/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── README.md
│       │   ├── asconfig.json
│       │   ├── assembly/
│       │   │   └── index.ts
│       │   └── package.json
│       ├── example-weather-plugin/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── README.md
│       │   ├── asconfig.json
│       │   ├── assembly/
│       │   │   └── index.ts
│       │   └── package.json
│       └── example-weather-provider/
│           ├── .gitignore
│           ├── .npmignore
│           ├── README.md
│           ├── asconfig.json
│           ├── assembly/
│           │   └── index.ts
│           └── package.json
├── fly_io/
│   ├── cr_signalk_io/
│   │   ├── Dockerfile
│   │   ├── fly.toml
│   │   └── nginx.conf
│   └── demo_signalk_org/
│       ├── Dockerfile
│       ├── fly.toml
│       └── security.json
├── index.js
├── kubernetes/
│   ├── README.md
│   ├── signalk-deployment.yaml
│   └── signalk-ingress.yaml
├── kubernetes.md
├── package.json
├── packages/
│   ├── assemblyscript-plugin-sdk/
│   │   ├── .npmignore
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── asconfig.json
│   │   ├── assembly/
│   │   │   ├── api.ts
│   │   │   ├── index.ts
│   │   │   ├── network.ts
│   │   │   ├── plugin.ts
│   │   │   ├── resources.ts
│   │   │   └── signalk.ts
│   │   ├── build/
│   │   │   ├── plugin.d.ts
│   │   │   └── plugin.js
│   │   └── package.json
│   ├── resources-provider-plugin/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── @types/
│   │   │   │   └── geojson-validation.d.ts
│   │   │   ├── index.ts
│   │   │   ├── openApi.json
│   │   │   └── types/
│   │   │       ├── index.ts
│   │   │       └── store.ts
│   │   └── tsconfig.json
│   ├── server-admin-ui/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── README.md
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public_src/
│   │   │   └── index.html
│   │   ├── scss/
│   │   │   ├── _bootstrap-variables.scss
│   │   │   ├── _core-variables.scss
│   │   │   ├── _custom.scss
│   │   │   ├── core/
│   │   │   │   ├── _animate.scss
│   │   │   │   ├── _aside.scss
│   │   │   │   ├── _avatars.scss
│   │   │   │   ├── _badge.scss
│   │   │   │   ├── _breadcrumb-menu.scss
│   │   │   │   ├── _breadcrumb.scss
│   │   │   │   ├── _buttons.scss
│   │   │   │   ├── _callout.scss
│   │   │   │   ├── _card.scss
│   │   │   │   ├── _charts.scss
│   │   │   │   ├── _dropdown-menu-right.scss
│   │   │   │   ├── _dropdown.scss
│   │   │   │   ├── _footer.scss
│   │   │   │   ├── _grid.scss
│   │   │   │   ├── _input-group.scss
│   │   │   │   ├── _layout.scss
│   │   │   │   ├── _loading.scss
│   │   │   │   ├── _mixins.scss
│   │   │   │   ├── _mobile.scss
│   │   │   │   ├── _modal.scss
│   │   │   │   ├── _nav.scss
│   │   │   │   ├── _navbar.scss
│   │   │   │   ├── _others.scss
│   │   │   │   ├── _progress.scss
│   │   │   │   ├── _rtl.scss
│   │   │   │   ├── _sidebar.scss
│   │   │   │   ├── _switches.scss
│   │   │   │   ├── _tables.scss
│   │   │   │   ├── _temp.scss
│   │   │   │   ├── _typography.scss
│   │   │   │   ├── _utilities.scss
│   │   │   │   ├── _variables.scss
│   │   │   │   ├── _widgets.scss
│   │   │   │   ├── core.scss
│   │   │   │   └── utilities/
│   │   │   │       ├── _background.scss
│   │   │   │       ├── _borders.scss
│   │   │   │       └── _display.scss
│   │   │   ├── style.scss
│   │   │   └── vendors/
│   │   │       ├── _variables.scss
│   │   │       └── chart.js/
│   │   │           └── chart.scss
│   │   ├── src/
│   │   │   ├── actions.ts
│   │   │   ├── blinking-circle.css
│   │   │   ├── bootstrap.tsx
│   │   │   ├── components/
│   │   │   │   ├── Aside/
│   │   │   │   │   └── Aside.tsx
│   │   │   │   ├── Footer/
│   │   │   │   │   └── Footer.tsx
│   │   │   │   ├── Header/
│   │   │   │   │   └── Header.tsx
│   │   │   │   ├── Icons.tsx
│   │   │   │   ├── Sidebar/
│   │   │   │   │   └── Sidebar.tsx
│   │   │   │   ├── SidebarFooter/
│   │   │   │   │   └── SidebarFooter.tsx
│   │   │   │   ├── SidebarForm/
│   │   │   │   │   └── SidebarForm.tsx
│   │   │   │   ├── SidebarHeader/
│   │   │   │   │   └── SidebarHeader.tsx
│   │   │   │   └── SidebarMinimizer/
│   │   │   │       └── SidebarMinimizer.tsx
│   │   │   ├── containers/
│   │   │   │   └── Full/
│   │   │   │       └── Full.tsx
│   │   │   ├── contexts/
│   │   │   │   └── WebSocketContext.tsx
│   │   │   ├── dataFetching.ts
│   │   │   ├── dependency-sync.test.ts
│   │   │   ├── fa-pulse.css
│   │   │   ├── hooks/
│   │   │   │   └── useWebSocket.ts
│   │   │   ├── index.ts
│   │   │   ├── routes.ts
│   │   │   ├── services/
│   │   │   │   └── WebSocketService.ts
│   │   │   ├── store/
│   │   │   │   ├── index.ts
│   │   │   │   ├── slices/
│   │   │   │   │   ├── appSlice.test.ts
│   │   │   │   │   ├── appSlice.ts
│   │   │   │   │   ├── dataSlice.test.ts
│   │   │   │   │   ├── dataSlice.ts
│   │   │   │   │   ├── prioritiesSlice.test.ts
│   │   │   │   │   ├── prioritiesSlice.ts
│   │   │   │   │   ├── unitPreferencesSlice.ts
│   │   │   │   │   ├── wsSlice.test.ts
│   │   │   │   │   └── wsSlice.ts
│   │   │   │   └── types.ts
│   │   │   ├── test/
│   │   │   │   └── setup.ts
│   │   │   ├── types/
│   │   │   │   └── jsonlint-mod.d.ts
│   │   │   ├── utils/
│   │   │   │   └── unitConversion.ts
│   │   │   └── views/
│   │   │       ├── Configuration/
│   │   │       │   ├── Configuration.tsx
│   │   │       │   └── EmbeddedPluginConfigurationForm.tsx
│   │   │       ├── Dashboard/
│   │   │       │   └── Dashboard.tsx
│   │   │       ├── DataBrowser/
│   │   │       │   ├── CopyToClipboardWithFade.tsx
│   │   │       │   ├── DataBrowser.tsx
│   │   │       │   ├── DataRow.tsx
│   │   │       │   ├── GranularSubscriptionManager.ts
│   │   │       │   ├── Meta.tsx
│   │   │       │   ├── TimestampCell.tsx
│   │   │       │   ├── ValueRenderers.tsx
│   │   │       │   ├── VirtualTable.css
│   │   │       │   ├── VirtualizedDataTable.tsx
│   │   │       │   ├── VirtualizedMetaTable.tsx
│   │   │       │   ├── pathUtils.ts
│   │   │       │   └── usePathData.ts
│   │   │       ├── Playground.tsx
│   │   │       ├── ServerConfig/
│   │   │       │   ├── BackupRestore.tsx
│   │   │       │   ├── BasicProvider.tsx
│   │   │       │   ├── Logging.tsx
│   │   │       │   ├── N2KFilters.tsx
│   │   │       │   ├── PluginConfigurationForm.tsx
│   │   │       │   ├── ProvidersConfiguration.tsx
│   │   │       │   ├── ServerLog.tsx
│   │   │       │   ├── ServerUpdate.tsx
│   │   │       │   ├── Settings.tsx
│   │   │       │   ├── SourcePriorities.tsx
│   │   │       │   ├── UnitPreferencesSettings.tsx
│   │   │       │   └── VesselConfiguration.tsx
│   │   │       ├── Webapps/
│   │   │       │   ├── Embedded.tsx
│   │   │       │   ├── EmbeddedAsyncApi.tsx
│   │   │       │   ├── EmbeddedDocs.tsx
│   │   │       │   ├── Webapp.tsx
│   │   │       │   ├── Webapps.tsx
│   │   │       │   ├── dynamicutilities.ts
│   │   │       │   └── loadingerror.tsx
│   │   │       ├── appstore/
│   │   │       │   ├── Apps/
│   │   │       │   │   ├── Apps.tsx
│   │   │       │   │   └── WarningBox.tsx
│   │   │       │   ├── AppsList.tsx
│   │   │       │   ├── Grid/
│   │   │       │   │   └── cell-renderers/
│   │   │       │   │       ├── ActionCellRenderer.test.tsx
│   │   │       │   │       └── ActionCellRenderer.tsx
│   │   │       │   └── appStore.scss
│   │   │       └── security/
│   │   │           ├── AccessRequests.tsx
│   │   │           ├── Devices.tsx
│   │   │           ├── EnableSecurity.tsx
│   │   │           ├── Login.tsx
│   │   │           ├── OIDCSettings.tsx
│   │   │           ├── Register.tsx
│   │   │           ├── Settings.tsx
│   │   │           └── Users.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── server-admin-ui-dependencies/
│   │   ├── index.js
│   │   └── package.json
│   ├── server-api/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── .npmrc
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── autopilotapi.ts
│   │   │   ├── brand.ts
│   │   │   ├── course.ts
│   │   │   ├── coursetypes.ts
│   │   │   ├── deltas.test.ts
│   │   │   ├── deltas.ts
│   │   │   ├── features.ts
│   │   │   ├── history.ts
│   │   │   ├── index.ts
│   │   │   ├── mmsi/
│   │   │   │   ├── mid.ts
│   │   │   │   ├── mmsi.test.ts
│   │   │   │   └── mmsi.ts
│   │   │   ├── notificationsapi.ts
│   │   │   ├── plugin.ts
│   │   │   ├── propertyvalues.test.ts
│   │   │   ├── propertyvalues.ts
│   │   │   ├── radarapi.ts
│   │   │   ├── resourcesapi.ts
│   │   │   ├── resourcetypes.ts
│   │   │   ├── serverapi.ts
│   │   │   ├── streambundle.ts
│   │   │   ├── subscriptionmanager.ts
│   │   │   ├── typebox/
│   │   │   │   ├── autopilot-schemas.ts
│   │   │   │   ├── course-schemas.ts
│   │   │   │   ├── discovery-schemas.ts
│   │   │   │   ├── history-schemas.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── notifications-schemas.ts
│   │   │   │   ├── protocol-schemas.ts
│   │   │   │   ├── radar-schemas.ts
│   │   │   │   ├── resources-schemas.ts
│   │   │   │   ├── shared-schemas.ts
│   │   │   │   └── weather-schemas.ts
│   │   │   ├── weatherapi.guard.ts
│   │   │   └── weatherapi.ts
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   └── wit/
│   │       └── signalk.wit
│   ├── streams/
│   │   ├── .npmrc
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── actisense-serial.ts
│   │   │   ├── autodetect.test.ts
│   │   │   ├── autodetect.ts
│   │   │   ├── canboatjs.test.ts
│   │   │   ├── canboatjs.ts
│   │   │   ├── canbus.ts
│   │   │   ├── execute.test.ts
│   │   │   ├── execute.ts
│   │   │   ├── filestream.ts
│   │   │   ├── folderstream.ts
│   │   │   ├── from_json.test.ts
│   │   │   ├── from_json.ts
│   │   │   ├── gpiod-seatalk.ts
│   │   │   ├── gpsd.test.ts
│   │   │   ├── gpsd.ts
│   │   │   ├── index.ts
│   │   │   ├── keys-filter.test.ts
│   │   │   ├── keys-filter.ts
│   │   │   ├── liner.test.ts
│   │   │   ├── liner.ts
│   │   │   ├── log.ts
│   │   │   ├── logging.test.ts
│   │   │   ├── logging.ts
│   │   │   ├── mdns-ws.test.ts
│   │   │   ├── mdns-ws.ts
│   │   │   ├── multiplexedlog.ts
│   │   │   ├── n2k-signalk.test.ts
│   │   │   ├── n2k-signalk.ts
│   │   │   ├── n2kAnalyzer.ts
│   │   │   ├── nmea0183-signalk.test.ts
│   │   │   ├── nmea0183-signalk.ts
│   │   │   ├── nullprovider.ts
│   │   │   ├── pigpio-seatalk.ts
│   │   │   ├── replacer.test.ts
│   │   │   ├── replacer.ts
│   │   │   ├── s3.ts
│   │   │   ├── serialport.ts
│   │   │   ├── simple.ts
│   │   │   ├── splitting-liner.test.ts
│   │   │   ├── splitting-liner.ts
│   │   │   ├── tcp.test.ts
│   │   │   ├── tcp.ts
│   │   │   ├── tcpserver.ts
│   │   │   ├── test-helpers.ts
│   │   │   ├── throttle.ts
│   │   │   ├── timestamp-throttle.test.ts
│   │   │   ├── timestamp-throttle.ts
│   │   │   ├── types.ts
│   │   │   ├── udp.test.ts
│   │   │   ├── udp.ts
│   │   │   └── vendor.d.ts
│   │   └── tsconfig.json
│   └── typedoc-theme/
│       ├── .gitignore
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       ├── src/
│       │   ├── SignalKTheme.tsx
│       │   ├── SignalKThemeContext.tsx
│       │   ├── assets/
│       │   │   └── theme.css
│       │   ├── index.tsx
│       │   └── partials/
│       │       └── toolbar.tsx
│       └── tsconfig.json
├── public/
│   └── examples/
│       ├── http-example.html
│       ├── index.html
│       └── loginform.html
├── releasing.md
├── samples/
│   ├── aava-n2k.data
│   ├── gofree-merrimac.log
│   ├── gps.log
│   ├── n2kd-183-merrimac.log
│   ├── nais300-merrimac.log
│   ├── nais400-merrimac.log
│   └── plaka.log
├── settings/
│   ├── actisense-serial-settings.json
│   ├── commandline-provider-settings.json
│   ├── defaults.json-sample
│   ├── multiple-sources.json
│   ├── multiplexed.json
│   ├── n2k-from-file-settings.json
│   ├── signalk-ws-settings.json
│   ├── simulator.json
│   ├── volare-file-settings-filtered.json
│   ├── volare-file-settings.json
│   ├── volare-gpsd-settings.json
│   ├── volare-serial-settings.json
│   ├── volare-tcp-settings.json
│   └── volare-udp-settings.json
├── src/
│   ├── @types/
│   │   ├── api-schema-builder.d.ts
│   │   ├── primus.d.ts
│   │   └── signalk_signalk-schema.d.ts
│   ├── BackpressureManager.ts
│   ├── LatestValuesAccumulator.ts
│   ├── api/
│   │   ├── apps/
│   │   │   ├── openApi.json
│   │   │   └── openApi.ts
│   │   ├── autopilot/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── course/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── discovery/
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── history/
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── index.ts
│   │   ├── notifications/
│   │   │   ├── alarm.ts
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   ├── notificationManager.ts
│   │   │   └── openApi.ts
│   │   ├── openApiSchemas.ts
│   │   ├── radar/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── resources/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   ├── openApi.ts
│   │   │   └── validate.ts
│   │   ├── security/
│   │   │   ├── openApi.json
│   │   │   └── openApi.ts
│   │   ├── streams/
│   │   │   ├── binary-stream-manager.ts
│   │   │   └── index.ts
│   │   ├── swagger.ts
│   │   └── weather/
│   │       ├── index.ts
│   │       └── openApi.ts
│   ├── app.ts
│   ├── atomicWrite.ts
│   ├── baconjs-compat.ts
│   ├── categories.ts
│   ├── config/
│   │   ├── config.test.js
│   │   ├── config.ts
│   │   ├── development.js
│   │   ├── get.js
│   │   └── production.js
│   ├── constants.ts
│   ├── cors.ts
│   ├── debug.ts
│   ├── deltaPriority.ts
│   ├── deltacache.ts
│   ├── deltachain.ts
│   ├── deltaeditor.ts
│   ├── deltastats.ts
│   ├── discovery.js
│   ├── dummysecurity.ts
│   ├── events.ts
│   ├── index.ts
│   ├── interfaces/
│   │   ├── applicationData.js
│   │   ├── appstore.js
│   │   ├── index.js
│   │   ├── logfiles.js
│   │   ├── mfd_webapp.ts
│   │   ├── nmea-tcp.ts
│   │   ├── playground.js
│   │   ├── plugins.ts
│   │   ├── providers.ts
│   │   ├── rest.js
│   │   ├── tcp.ts
│   │   ├── unitpreferences-api.js
│   │   ├── wasm.ts
│   │   ├── webapps.js
│   │   └── ws.ts
│   ├── logging.js
│   ├── login-rate-limiter.ts
│   ├── mdns.js
│   ├── modules.ts
│   ├── oidc/
│   │   ├── authorization.ts
│   │   ├── config.ts
│   │   ├── discovery.ts
│   │   ├── id-token-validation.ts
│   │   ├── index.ts
│   │   ├── oidc-admin.ts
│   │   ├── oidc-auth.ts
│   │   ├── permission-mapping.ts
│   │   ├── pkce.ts
│   │   ├── state.ts
│   │   ├── token-exchange.ts
│   │   ├── types.ts
│   │   └── user-info.ts
│   ├── pipedproviders.ts
│   ├── plugin-paths.ts
│   ├── pluginid.ts
│   ├── ports.ts
│   ├── put.ts
│   ├── redirects.json
│   ├── requestResponse.ts
│   ├── security.ts
│   ├── serialports.ts
│   ├── serverroutes.ts
│   ├── serverstate/
│   │   └── store.ts
│   ├── streambundle.ts
│   ├── subscriptionmanager.ts
│   ├── tokensecurity.ts
│   ├── types.ts
│   ├── unitpreferences/
│   │   ├── index.ts
│   │   ├── loader.ts
│   │   ├── resolver.ts
│   │   └── types.ts
│   ├── version.ts
│   ├── wasm/
│   │   ├── bindings/
│   │   │   ├── binary-stream.ts
│   │   │   ├── env-imports.ts
│   │   │   ├── index.ts
│   │   │   ├── radar-provider.ts
│   │   │   ├── resource-provider.ts
│   │   │   ├── socket-manager.ts
│   │   │   └── weather-provider.ts
│   │   ├── index.ts
│   │   ├── loader/
│   │   │   ├── index.ts
│   │   │   ├── plugin-config.ts
│   │   │   ├── plugin-lifecycle.ts
│   │   │   ├── plugin-registry.ts
│   │   │   ├── plugin-routes.ts
│   │   │   └── types.ts
│   │   ├── loaders/
│   │   │   ├── index.ts
│   │   │   └── standard-loader.ts
│   │   ├── types.ts
│   │   ├── utils/
│   │   │   ├── fetch-wrapper.ts
│   │   │   ├── format-detection.ts
│   │   │   └── index.ts
│   │   ├── wasm-runtime.ts
│   │   ├── wasm-serverapi.ts
│   │   ├── wasm-storage.ts
│   │   └── wasm-subscriptions.ts
│   ├── zip.ts
│   └── zones.ts
├── test/
│   ├── BackpressureManager.ts
│   ├── LatestValuesAccumulator.js
│   ├── acls.js
│   ├── applicationData.ts
│   ├── chart-tile-regex.ts
│   ├── course.ts
│   ├── delete.js
│   ├── deltaPriority.ts
│   ├── deltacache.js
│   ├── endpoint-auth.ts
│   ├── error-logging.ts
│   ├── externalssl.ts
│   ├── filter-test-helper.ts
│   ├── history-api.ts
│   ├── history.js
│   ├── httpprovider.js
│   ├── metadata-e2e.ts
│   ├── metadata.js
│   ├── modules.js
│   ├── multiple-values.js
│   ├── nmea0183-filtering.ts
│   ├── notifications.ts
│   ├── oidc/
│   │   ├── authorization.test.ts
│   │   ├── config.test.ts
│   │   ├── crypto-service.test.ts
│   │   ├── discovery.test.ts
│   │   ├── id-token-validation.test.ts
│   │   ├── integration.test.ts
│   │   ├── oidc-auth.test.ts
│   │   ├── permission-mapping.test.ts
│   │   ├── pkce.test.ts
│   │   ├── settings-api.test.ts
│   │   ├── state.test.ts
│   │   ├── token-exchange.test.ts
│   │   ├── user-info.test.ts
│   │   ├── user-service.test.ts
│   │   └── userinfo-validation.test.ts
│   ├── plugin-crash-isolation.ts
│   ├── plugin-test-config/
│   │   └── package.json
│   ├── plugins.js
│   ├── providers.js
│   ├── put.js
│   ├── rate-limit.ts
│   ├── resources.ts
│   ├── scripts/
│   │   ├── mock-systemctl
│   │   └── signalk-server-setup
│   ├── seatalk1-filtering.ts
│   ├── security.js
│   ├── server-test-config/
│   │   ├── .npmrc
│   │   └── package.json
│   ├── servertestutilities.js
│   ├── sliding-session.ts
│   ├── ssl.ts
│   ├── staticData.js
│   ├── subscriptions.js
│   ├── ts-servertestutilities.ts
│   ├── unitpreferences.ts
│   ├── wasm-plugin-test-config/
│   │   └── package.json
│   ├── wasm-plugins.ts
│   ├── ws-connection-limit.ts
│   └── zones.ts
├── test-server-as-include/
│   ├── package.json
│   ├── run.sh
│   └── works-as-include.js
├── tools/
│   ├── README.md
│   ├── oidc-test-env/
│   │   ├── README.md
│   │   ├── authelia/
│   │   │   ├── configuration.yml
│   │   │   └── users_database.yml
│   │   ├── docker-compose.yml
│   │   └── traefik/
│   │       ├── dynamic.yml
│   │       └── traefik.yml
│   ├── test-auth-negative.sh
│   ├── test-oidc-all.sh
│   ├── test-oidc-flow.sh
│   └── test-oidc-sso.sh
├── tsconfig.base.json
├── tsconfig.json
├── typedoc.json
├── unitpreferences/
│   ├── README.md
│   ├── categories.json
│   ├── config.json
│   ├── custom-categories.json
│   ├── default-categories.json
│   ├── presets/
│   │   ├── imperial-uk.json
│   │   ├── imperial-us.json
│   │   ├── metric.json
│   │   ├── nautical-imperial-uk.json
│   │   ├── nautical-imperial-us.json
│   │   └── nautical-metric.json
│   ├── primary-categories.json
│   └── standard-units-definitions.json
└── util/
    └── start-stop.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .coderabbit.yaml
================================================
language: en-US

reviews:
  profile: assertive
  auto_review:
    enabled: true
    drafts: false
  high_level_summary_instructions: |
    Write all summaries in present tense.
    Describe what the code does, not what it did.
    Example: "This PR adds feature X" instead of "This PR added feature X"

  path_instructions:
    - path: '**/*'
      instructions: |
        ## Contribution guidelines compliance
        Check that the PR follows the SignalK contribution guidelines:
        https://github.com/SignalK/signalk-server/blob/master/CONTRIBUTING.md
        Flag any deviation in PR structure, commit message format, or documentation
        requirements.

        ## Echo comments
        Flag any comment that merely restates what the code already says.
        Examples of echo comments to flag:
        - `// Sets the age` above a function named `setAge()`
        - `// Loop through items` above a `for` loop
        These add noise without adding meaning. Request removal or replacement
        with a comment explaining *why*, not *what*.

        ## Leftover crumbs from intermediate commits
        Check for references to things that existed in earlier commits of this PR
        but are no longer present — removed variables, old function names, deleted
        files, superseded approaches. These are confusing to future readers.
        Flag any comments, docs, or code that refer to something not present in
        the current state of the branch.

        ## Documentation drift risk
        Flag any .md file that contains detailed implementation steps, specific
        API call sequences, code snippets, or configuration values that are likely
        to fall out of sync as the code evolves. Documentation should describe
        architecture and how things work conceptually — not step-by-step
        instructions that duplicate or shadow the code itself.

        ## Unchecked items in test plans
        If a PR description or any .md file contains a checklist with unchecked
        items, flag it. Either the work is incomplete, or the checklist should be
        removed before merge. Do not let unchecked boxes pass silently.

        ## Implementation status in documentation
        Flag any .md file that describes implementation progress, status, or
        build steps (e.g. "Step 3: implement X", "TODO: add Y", "currently
        implemented as Z"). This belongs in PR descriptions or commit messages,
        not in documentation. Documentation should describe how things work,
        not how they were built or what stage they are in.
        Architecture decisions and design rationale are fine. Build narratives
        are not.

        ## What NOT to flag
        Do not flag the following patterns — they are intentional:
        - Constant names that are descriptive in their own context (e.g.
          DEFAULT_PULL_TIMEOUT) — do not suggest renames for clarity.
        - Test cleanup using Object.assign(process.env, original) — this is
          sufficient when the test only reads from process.env.
        - Test assertions that verify array length and structure rather than
          exact element order — Object property iteration order is guaranteed
          for string keys in modern JS.
        - Smoke tests that verify a method does not throw without asserting
          on internal state — exposing internals for testing is worse.
        - Callbacks typed as functions that only work in-process — do not
          suggest REST/WebSocket alternatives unless specifically requested.
          The limitation is obvious from the type signature.

        ## Scope
        Focus on files changed since master. Think carefully about how changes
        interact with existing code and documentation — not just the diff in
        isolation.


================================================
FILE: .dockerignore
================================================
node_modules
packages
work


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .gitattributes
================================================
# Default: normalize line endings to LF in the repo, auto-detect on checkout
* text=auto eol=lf

# Force LF for files that must never be CRLF
*.sh text eol=lf
*.bash text eol=lf
*.json text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf
*.conf text eol=lf
Dockerfile text eol=lf

# Binary files — do not normalize
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary


================================================
FILE: .github/FUNDING.yml
================================================
github: [sbender9, tkurki]


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  # Enable version updates for npm
  - package-ecosystem: 'npm'
    # Look for `package.json` and `lock` files in the `root` directory
    directory: '/'
    # Check the npm registry for updates every day (weekdays)
    schedule:
      interval: 'monthly'
    open-pull-requests-limit: 5

  # Enable version updates for Docker
  - package-ecosystem: 'docker'
    # Look for a `Dockerfile` in the `root` directory
    directory: '/'
    # Check for updates once a week
    schedule:
      interval: 'monthly'

  # Enable version updates for GitHub Actions
  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'monthly'


================================================
FILE: .github/workflows/build-base-image.yml
================================================
name: Build Docker base images

on:
  schedule:
    - cron: '0 0 * * 1'
  workflow_dispatch:

jobs:
  build-images:
    if: github.repository == 'SignalK/signalk-server'
    name: Ubuntu base image
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        vm: [ubuntu-latest, ubuntu-24.04-arm]
        include:
          - vm: ubuntu-latest
            arch: amd
            os: 24.04
            node: 24.x
            node_safe: 24.x
            platform: linux/amd64
          - vm: ubuntu-latest
            arch: amd
            os: alpine
            node: 24.x
            node_safe: 24
            platform: linux/amd64
          - vm: ubuntu-24.04-arm
            arch: arm
            os: 24.04
            node: 24.x
            node_safe: 24.x
            platform: linux/arm64
          - vm: ubuntu-24.04-arm
            arch: arm
            os: alpine
            node: 24.x
            node_safe: 24
            platform: linux/arm64
    runs-on: ${{ matrix.vm }}
    steps:
      - uses: actions/checkout@v6
      - uses: docker/setup-buildx-action@v4

      - name: Login to ghcr.io
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_PAT }}

      - name: Build Ubuntu baseimages
        uses: docker/build-push-action@v7
        with:
          context: .
          file: ./docker/Dockerfile_base_${{ matrix.os }}
          platforms: ${{ matrix.platform }}
          push: true
          tags: |
            ghcr.io/signalk/signalk-server-base:${{ matrix.arch }}-${{ matrix.os }}-${{ matrix.node_safe }}
          build-args: |
            NODE=${{ matrix.node_safe }}

  create-and-push-manifest:
    needs: [build-images]
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            node_safe: 24

    steps:
      - name: Login to ghcr.io
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_PAT }}

      - name: Create and push multi-arch manifest to GHCR
        uses: int128/docker-manifest-create-action@v2
        with:
          tags: |
            ghcr.io/signalk/signalk-server-base:latest-${{ matrix.os }}-${{ matrix.node_safe }}
          sources: |
            ghcr.io/signalk/signalk-server-base:amd-${{ matrix.os }}-${{ matrix.node_safe }}
            ghcr.io/signalk/signalk-server-base:arm-${{ matrix.os }}-${{ matrix.node_safe }}

  copy-to-dockerhub:
    needs: create-and-push-manifest
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            node_safe: 24

    steps:
      - name: Install skopeo
        run: |
          sudo apt-get update
          sudo apt-get install -y skopeo
      - name: Copy images from GHCR to Docker Hub
        shell: bash
        env:
          GHCR_USERNAME: ${{ github.actor }}
          GHCR_TOKEN: ${{ secrets.GHCR_PAT }}
          DOCKER_HUB_USERNAME: signalkci
          DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
        run: |
          set -euo pipefail
          skopeo copy --all \
            --src-creds "${GHCR_USERNAME}:${GHCR_TOKEN}" \
            --dest-creds "${DOCKER_HUB_USERNAME}:${DOCKER_HUB_ACCESS_TOKEN}" \
            "docker://ghcr.io/signalk/signalk-server-base:latest-${{ matrix.os }}-${{ matrix.node_safe }}" \
            "docker://docker.io/signalk/signalk-server-base:latest-${{ matrix.os }}-${{ matrix.node_safe }}"

  housekeeping:
    needs: [copy-to-dockerhub]
    runs-on: ubuntu-latest
    permissions:
      packages: write

    steps:
      - name: Wait for GHCR indexing
        run: sleep 60
      - name: Remove Docker Image from GHCR
        continue-on-error: true
        uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          packages: signalk-server-base
          delete-untagged: true
          delete-tags: |
            amd-*,arm-*
          token: ${{ secrets.GHCR_PAT }} # Need to have delete permission


================================================
FILE: .github/workflows/build-docker.yml
================================================
name: Build Docker development container

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  push:
    branches:
      - master
      - 'build-docker'
    tags:
      - '*'
      - '!v*'
  workflow_dispatch:

jobs:
  signalk-server_npm_files:
    if: github.repository == 'SignalK/signalk-server'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Node setup
        uses: actions/setup-node@v6
        with:
          node-version: '24.x'
      - name: Build npm files locally and upload artifacts
        run: |
          npm cache clean -f
          npm install npm@latest -g
          npm install --package-lock-only
          npm ci && npm cache clean --force
          npm run build:all
          npm pack --workspaces
          rm signalk-typedoc-signalk-theme*.tgz # This is only needed as a dev dependency
          npm pack
      - name: Upload artifacts
        uses: actions/upload-artifact@v7
        with:
          retention-days: 1
          name: packed-modules
          path: |
            *.tgz

  docker_images:
    needs: signalk-server_npm_files
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        vm: [ubuntu-latest, ubuntu-24.04-arm]
        include:
          - vm: ubuntu-latest
            arch: amd
            platform: linux/amd64
          - vm: ubuntu-24.04-arm
            arch: arm
            node: 24.x
            platform: linux/arm64
          - os: 24.04
            node: 24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            node_safe: 24

    runs-on: ${{ matrix.vm }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Login to ghcr.io
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_PAT }}
      - uses: actions/download-artifact@v8
        with:
          name: packed-modules
      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          file: ./docker/Dockerfile
          platforms: ${{ matrix.platform }}
          push: true
          tags: ghcr.io/signalk/signalk-server:${{ matrix.arch }}-${{ matrix.os }}-${{ matrix.node_safe }}-${{ github.run_id }}
          build-args: |
            REGISTRY=ghcr.io
            BASE_IMAGE=${{ matrix.os }}-${{ matrix.node_safe }}

  create-and-push-manifest:
    needs: docker_images
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            suffix: -24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            suffix: -24-alpine
            node_safe: 24

    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Docker meta
        id: docker_meta
        uses: docker/metadata-action@v6
        with:
          images: |
            ghcr.io/signalk/signalk-server
          tags: |
            type=ref,event=branch
            type=sha
          flavor: |
            suffix=${{ matrix.suffix }}
      - name: Login to ghcr.io
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_PAT }}
      - name: Create and push multi-arch manifest to GHCR
        uses: int128/docker-manifest-create-action@v2
        with:
          tags: |
            ${{ steps.docker_meta.outputs.tags }}
          sources: |
            ghcr.io/signalk/signalk-server:amd-${{ matrix.os }}-${{ matrix.node_safe }}-${{ github.run_id }}
            ghcr.io/signalk/signalk-server:arm-${{ matrix.os }}-${{ matrix.node_safe }}-${{ github.run_id }}
      - name: Save tags to file
        run: |
          mkdir -p /tmp/tags
          echo "${{ steps.docker_meta.outputs.tags }}" > /tmp/tags/${{ matrix.node_safe }}.txt
      - name: Upload tag artifact
        uses: actions/upload-artifact@v7
        with:
          name: ubuntu-tag-${{ matrix.node_safe }}
          path: /tmp/tags/${{ matrix.node_safe }}.txt
          retention-days: 1

  copy-to-dockerhub:
    needs: create-and-push-manifest
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            node_safe: 24
    steps:
      - name: Download tag artifact
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-tag-${{ matrix.node_safe }}
          path: /tmp/tags
      - name: Install skopeo
        run: |
          sudo apt-get update
          sudo apt-get install -y skopeo
      - name: Copy images from GHCR to Docker Hub
        shell: bash
        env:
          GHCR_USERNAME: ${{ github.actor }}
          GHCR_TOKEN: ${{ secrets.GHCR_PAT }}
          DOCKER_HUB_USERNAME: signalkci
          DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
        run: |
          set -euo pipefail
          TAGS_FILE="/tmp/tags/${{ matrix.node_safe }}.txt"
          while IFS= read -r FULL_TAG || [ -n "${FULL_TAG:-}" ]; do
            [ -z "${FULL_TAG:-}" ] && continue
            TAG="${FULL_TAG##*:}"
            echo "Copying: ${FULL_TAG} -> signalk/signalk-server:${TAG}"
            skopeo copy --all \
              --src-creds "${GHCR_USERNAME}:${GHCR_TOKEN}" \
              --dest-creds "${DOCKER_HUB_USERNAME}:${DOCKER_HUB_ACCESS_TOKEN}" \
              "docker://${FULL_TAG}" \
              "docker://docker.io/signalk/signalk-server:${TAG}"
          done < "$TAGS_FILE"

  record-image-sizes:
    needs: create-and-push-manifest
    runs-on: ubuntu-latest
    steps:
      - name: Checkout metrics branch
        uses: actions/checkout@v6
        with:
          ref: metrics
      - name: Record image sizes
        env:
          GHCR_CREDS: '${{ github.actor }}:${{ secrets.GHCR_PAT }}'
        run: |
          set -euo pipefail
          TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
          COMMIT="${{ github.sha }}"
          CSV="master-container_sizes.csv"

          if [ ! -f "$CSV" ]; then
            echo "timestamp,commit,tag,size_bytes" > "$CSV"
          fi

          REGISTRY="ghcr.io/signalk/signalk-server"
          RUN_ID="${{ github.run_id }}"

          for TAG in \
            "amd-24.04-24.x-${RUN_ID}" \
            "arm-24.04-24.x-${RUN_ID}" \
            "amd-alpine-24-${RUN_ID}" \
            "arm-alpine-24-${RUN_ID}"; do

            DIGEST=$(skopeo inspect --raw --creds "${GHCR_CREDS}" \
              "docker://${REGISTRY}:${TAG}" | \
              jq -r '.manifests[0].digest')
            SIZE=$(skopeo inspect --raw --creds "${GHCR_CREDS}" \
              "docker://${REGISTRY}@${DIGEST}" | \
              jq '[.layers[].size] | add')

            echo "${TIMESTAMP},${COMMIT},${TAG%-${RUN_ID}},${SIZE}" >> "$CSV"
          done
      - name: Commit and push
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add master-container_sizes.csv
          git commit -m "chore: record container sizes for ${GITHUB_SHA::7}"
          git push

  housekeeping:
    needs: [copy-to-dockerhub, record-image-sizes]
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - name: Wait for GHCR indexing
        run: sleep 60
      - name: Remove Temporary & Untagged Docker Images from GHCR
        continue-on-error: true
        uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          packages: signalk-server
          delete-untagged: true
          delete-tags: |
            *-${{ github.run_id }}
          token: ${{ secrets.GHCR_PAT }}


================================================
FILE: .github/workflows/plugin-ci.yml
================================================
# SignalK Plugin CI - Reusable Workflow
#
# This workflow lives in the SignalK/signalk-server repository.
# Plugin developers reference it with a single line in their own workflow.
#
# Platforms tested:
#   - Linux x64    (Node 22, 24)
#   - Linux arm64  (Node 22, 24) — Raspberry Pi 4/5
#   - macOS        (Node 22, 24)
#   - Windows      (Node 22, 24)
#   - armv7/armhf  (Node 20 — matches Venus OS 3.70 on Cerbo GX)
#                    Runs under QEMU emulation with python3, make, g++
#
# Usage in your plugin repo:
#   See docs/develop/plugins/examples/plugin-caller-example.yml

name: SignalK Plugin CI

on:
  workflow_call:
    inputs:
      # ── Test configuration ──────────────────────────────────
      test-command:
        description: 'Command to run tests (default: npm test)'
        type: string
        default: 'npm test'
      build-command:
        description: 'Command to build the plugin (default: npm run build --if-present)'
        type: string
        default: 'npm run build --if-present'
      format-check-command:
        description: 'Command to verify formatting (e.g. "npm run prettier:check" or "npx biome check ."). Blocking when set, skipped when empty.'
        type: string
        default: ''
      coverage-command:
        description: 'Command that runs tests with coverage (e.g. "npm run coverage"). When set, replaces the standard test run and its output is appended to the job step summary.'
        type: string
        default: ''
      node-versions:
        description: 'JSON array of Node versions for desktop platforms (default: ["22", "24"])'
        type: string
        default: '["22", "24"]'

      # ── armv7 / Cerbo GX options ────────────────────────────
      enable-armv7:
        description: 'Run armv7 (Cerbo GX) tests via QEMU emulation'
        type: boolean
        default: true

      # ── SignalK integration test ────────────────────────────
      enable-signalk-integration:
        description: 'Start a SignalK server and install the plugin for integration testing'
        type: boolean
        default: false
      signalk-server-versions:
        description: 'JSON array of signalk-server versions to fan the integration test out over (e.g. ''["2.23.0", "latest"]'')'
        type: string
        default: '["latest"]'

# Least-privilege token for the jobs in this reusable workflow. We only
# read the caller's repository — install, build, and test steps never
# need to write to GitHub resources.
permissions:
  contents: read

jobs:
  # ════════════════════════════════════════════════════════════
  #  Desktop platforms matrix: Linux x64, Linux arm64, macOS, Windows
  # ════════════════════════════════════════════════════════════
  desktop:
    name: >-
      ${{ matrix.os == 'ubuntu-latest' && 'Linux' ||
          matrix.os == 'ubuntu-24.04-arm' && 'Linux arm64' ||
          matrix.os == 'macos-latest' && 'macOS' ||
          matrix.os == 'windows-latest' && 'Windows' ||
          matrix.os }} / Node ${{ matrix.node }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
        node: ${{ fromJson(inputs.node-versions) }}
    steps:
      - name: Checkout plugin
        uses: actions/checkout@v6

      - name: Setup Node.js ${{ matrix.node }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node }}
          cache: ${{ hashFiles('**/package-lock.json') != '' && 'npm' || '' }}

      - name: Validate plugin package.json
        id: validate-pkg
        shell: bash
        run: |
          cat << 'VALIDATE' > /tmp/validate-pkg.js
          const pkg = require(process.cwd() + '/package.json');
          const fs = require('fs');
          const path = require('path');
          let errors = [];
          let warnings = [];

          // Required: signalk-node-server-plugin keyword
          if (!pkg.keywords || !pkg.keywords.includes('signalk-node-server-plugin')) {
            errors.push('Missing required keyword: signalk-node-server-plugin');
          }

          // Required: main or exports field
          if (!pkg.main && !pkg.exports) {
            errors.push('Missing main or exports field');
          }

          // Version must be valid semver — npm normalizes invalid versions in
          // the registry but the installed package.json retains the original,
          // which breaks semver comparisons in the appstore
          const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
          if (!pkg.version) {
            errors.push('Missing "version" field in package.json');
          } else if (!SEMVER_RE.test(pkg.version)) {
            errors.push('Invalid semver version "' + pkg.version + '" in package.json — see https://semver.org');
          }

          // Scan source files for hardcoded home directory paths
          const srcFiles = [];
          function collectFiles(dir, exts) {
            if (!fs.existsSync(dir)) return;
            for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
              if (entry.name === 'node_modules' || entry.name === '.git') continue;
              const full = path.join(dir, entry.name);
              if (entry.isDirectory()) { collectFiles(full, exts); continue; }
              if (exts.some(e => entry.name.endsWith(e))) srcFiles.push(full);
            }
          }
          collectFiles('.', ['.js', '.ts', '.mjs', '.cjs', '.sh']);

          const hardcodedPathRe = /["'`]\/home\/[a-zA-Z][a-zA-Z0-9_-]*\//g;
          for (const file of srcFiles) {
            const content = fs.readFileSync(file, 'utf8');
            const matches = content.match(hardcodedPathRe);
            if (matches) {
              const relFile = path.relative('.', file);
              errors.push(relFile + ' contains hardcoded home directory path: ' + matches[0]);
            }
          }

          // Warn about install-time scripts that won't run under the App
          // Store's --ignore-scripts install. 'prepare' is intentionally
          // excluded — it's the standard TypeScript publish-build hook.
          const riskyScripts = ['preinstall', 'postinstall', 'install'];
          for (const s of riskyScripts) {
            if (pkg.scripts && pkg.scripts[s]) {
              warnings.push('package.json has a "' + s + '" script: ' + pkg.scripts[s]);
              warnings.push('Note: SignalK App Store uses --ignore-scripts, so "' + s + '" will NOT run on install');
            }
          }

          // engines.node
          if (!pkg.engines || !pkg.engines.node) {
            warnings.push('No "engines.node" field in package.json — plugins should declare their minimum Node.js version');
          }

          // ESM-only plugins can't be loaded by the server's require() call
          if (pkg.type === 'module') {
            warnings.push('package.json has "type": "module" — SignalK server loads plugins with require(). ESM-only plugins may fail to load.');
          }

          // baconjs is provided by the server — plugins must not bundle their own copy
          if (pkg.dependencies && pkg.dependencies.baconjs) {
            const baconRange = pkg.dependencies.baconjs;
            // Accept shorthand ranges ("^3", "3", "3.x") as well as full semver.
            // Only warn when we confidently parse a leading major < 3 — dist-tags,
            // git specs, and other non-numeric ranges are left alone.
            const baconMatch = baconRange.match(/^\s*[~^]?(\d+)(?:[.\s]|$)/);
            if (baconMatch) {
              const baconMajor = parseInt(baconMatch[1], 10);
              if (baconMajor < 3) {
                warnings.push(
                  'Plugin bundles baconjs ' + baconRange + ' in dependencies. ' +
                  'The Signal K server provides baconjs >= 3.x and forces all plugins to use its copy. ' +
                  'Bundling an older version will be ignored at runtime and wastes install size. ' +
                  'Remove baconjs from dependencies or update to ^3.0.0 and use the server-provided instance'
                );
              }
            }
          }

          // React 19 required for plugins with embedded webapps (Module Federation)
          const webappKeywords = ['signalk-plugin-configurator', 'signalk-embeddable-webapp', 'signalk-webapp'];
          const matchedWebappKeywords = pkg.keywords ? webappKeywords.filter(kw => pkg.keywords.includes(kw)) : [];
          if (matchedWebappKeywords.length > 0) {
            const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
            const reactRange = allDeps.react;
            if (reactRange) {
              // Accept shorthand ranges ("^18", "18", "18.x") as well as full semver.
              // Matches the baconjs check above — only warn when a leading major
              // parses confidently. Dist-tags and git specs are left alone.
              const versionMatch = reactRange.match(/^\s*[~^]?(\d+)(?:[.\s]|$)/);
              if (versionMatch) {
                const major = parseInt(versionMatch[1], 10);
                if (major < 19) {
                  warnings.push(
                    'Plugin has webapp keyword (' + matchedWebappKeywords.join(', ') +
                    ') but declares react ' + reactRange + ' (major version ' + major + '). ' +
                    'The Signal K admin UI requires React >= 19 for Module Federation compatibility. ' +
                    'Update your react and react-dom dependencies to ^19.0.0'
                  );
                }
              }
            }
          }

          warnings.forEach(w => console.log('::warning::' + w));
          if (errors.length > 0) {
            errors.forEach(e => console.log('::error::' + e));
            process.exit(1);
          }
          console.log('Plugin package.json validation passed');
          VALIDATE
          node /tmp/validate-pkg.js

      - name: Install dependencies
        run: |
          if [ -f package-lock.json ]; then
            npm ci
          else
            echo "No package-lock.json found — using npm install instead of npm ci"
            npm install
          fi
        shell: bash

      - name: Build
        env:
          BUILD_CMD: ${{ inputs.build-command }}
        shell: bash
        run: eval "$BUILD_CMD"

      - name: Validate plugin entry point
        id: validate-entry
        shell: bash
        timeout-minutes: 2
        run: |
          cat << 'ENTRYPOINT' > /tmp/validate-entry.js
          const path = require('path');
          const pkg = require(process.cwd() + '/package.json');
          // Resolve the plugin's CommonJS entry point, honoring conditional
          // exports like { ".": { "require": "./cjs/index.cjs" } } that
          // modern TypeScript templates emit. Falls back to 'index.js'.
          function resolveEntry(p, fallback) {
            if (p && p.main) return p.main;
            const e = p && p.exports;
            if (typeof e === 'string') return e;
            if (e && typeof e === 'object') {
              const root = typeof e['.'] !== 'undefined' ? e['.'] : e;
              if (typeof root === 'string') return root;
              if (root && typeof root === 'object') {
                for (const k of ['require', 'node', 'default', 'import']) {
                  if (typeof root[k] === 'string') return root[k];
                }
              }
            }
            return fallback;
          }
          const entry = resolveEntry(pkg, 'index.js');
          try {
            const mod = require(path.resolve(entry));
            const pluginConstructor = mod.default || mod;
            if (typeof pluginConstructor !== 'function') {
              console.log('::error::Plugin entry point does not export a function (got ' + typeof pluginConstructor + ')');
              process.exit(1);
            }
            console.log('Plugin exports a valid constructor function (' + entry + ')');
          } catch (e) {
            console.log('::error::Failed to load plugin entry point (' + entry + '): ' + e.message);
            process.exit(1);
          }
          process.exit(0);
          ENTRYPOINT
          node /tmp/validate-entry.js

      - name: Validate plugin.schema() if defined
        id: validate-schema
        shell: bash
        timeout-minutes: 2
        run: |
          cat << 'SCHEMACHECK' > /tmp/check-schema.js
          const path = require('path');
          const pkg = require(process.cwd() + '/package.json');
          // See validate-entry.js for the rationale — object/conditional
          // exports must be resolved, not flattened to 'index.js'.
          function resolveEntry(p, fallback) {
            if (p && p.main) return p.main;
            const e = p && p.exports;
            if (typeof e === 'string') return e;
            if (e && typeof e === 'object') {
              const root = typeof e['.'] !== 'undefined' ? e['.'] : e;
              if (typeof root === 'string') return root;
              if (root && typeof root === 'object') {
                for (const k of ['require', 'node', 'default', 'import']) {
                  if (typeof root[k] === 'string') return root[k];
                }
              }
            }
            return fallback;
          }
          const entry = resolveEntry(pkg, 'index.js');

          let mod;
          try {
            mod = require(path.resolve(entry));
          } catch (e) {
            // Entry point already validated in previous step — skip schema check
            console.log('Could not load plugin — skipping schema check');
            process.exit(0);
          }

          const ctor = mod.default || mod;
          if (typeof ctor !== 'function') {
            process.exit(0);
          }

          // Minimal mock — only needs to survive the constructor
          const mockApp = {
            config: { configPath: '/tmp', version: '0.0.0' },
            selfContext: 'vessels.urn:mrn:signalk:uuid:test',
            getPath: () => '',
            getSelfPath: () => undefined,
            getMetadata: () => undefined,
            getDataDirPath: () => '/tmp',
            readPluginOptions: () => ({}),
            savePluginOptions: (config, cb) => { if (cb) cb(null); },
            handleMessage: () => {},
            debug: () => {},
            error: () => {},
            setPluginStatus: () => {},
            setPluginError: () => {},
            reportError: () => {},
            on: () => ({ unsubscribe: () => {} }),
            registerPutHandler: () => {},
            registerDeltaInputHandler: () => () => {},
            subscriptionmanager: { subscribe: () => ({ unsubscribe: () => {} }) },
            streambundle: { getSelfBus: () => ({ onValue: () => ({ unsubscribe: () => {} }) }) },
            signalk: { self: 'vessels.urn:mrn:signalk:uuid:test', retrieve: () => ({}) }
          };

          let plugin;
          try {
            plugin = ctor(mockApp);
          } catch (e) {
            // Constructor needs a real app — we test lifecycle separately
            console.log('Plugin constructor needs server context — skipping schema check');
            process.exit(0);
          }

          // plugin.schema can be a function or a plain object (both are valid)
          if (!plugin || (typeof plugin.schema !== 'function' && typeof plugin.schema !== 'object') || plugin.schema === null) {
            console.log('Plugin does not define schema — nothing to check');
            process.exit(0);
          }

          const schemaIsFunction = typeof plugin.schema === 'function';
          const label = schemaIsFunction ? 'plugin.schema()' : 'plugin.schema';
          let schema;

          if (schemaIsFunction) {
            // A throwing schema() crashes the server's plugin config UI
            try {
              schema = plugin.schema();
            } catch (e) {
              console.log('::error::' + label + ' threw an error: ' + e.message);
              console.log('::error::This will crash the SignalK server when it tries to render the plugin config UI.');
              process.exit(1);
            }
          } else {
            schema = plugin.schema;
          }

          // Validate it returned something useful
          if (schema === null || schema === undefined) {
            console.log('::warning::' + label + ' returned ' + schema + ' — consider returning a valid JSON Schema object');
            process.exit(0);
          }

          if (typeof schema !== 'object') {
            console.log('::error::' + label + ' returned ' + typeof schema + ' instead of a JSON Schema object');
            process.exit(1);
          }

          // Basic JSON Schema structure check (rjsf compatibility)
          if (!schema.type && !schema.properties && !schema.oneOf && !schema.anyOf) {
            console.log('::warning::' + label + ' has no type, properties, oneOf, or anyOf — is this a valid JSON Schema?');
          }

          // Check for JSON-hostile values that would be silently dropped or
          // replaced by JSON.stringify. Circular refs throw from stringify;
          // functions/symbols/undefined properties do not — walk the tree
          // explicitly so authors get a useful error instead of data loss.
          const jsonIssues = [];
          const seen = new WeakSet();
          function walk(node, path) {
            if (node === null) return;
            const t = typeof node;
            if (t === 'function' || t === 'symbol') {
              jsonIssues.push(path + ' is a ' + t + ' — JSON.stringify will drop it');
              return;
            }
            if (t !== 'object') return;
            if (seen.has(node)) {
              jsonIssues.push(path + ' is a circular reference');
              return;
            }
            seen.add(node);
            if (Array.isArray(node)) {
              node.forEach((v, i) => walk(v, path + '[' + i + ']'));
              return;
            }
            for (const k of Object.keys(node)) {
              const v = node[k];
              if (v === undefined) {
                jsonIssues.push(path + '.' + k + ' is undefined — JSON.stringify will drop this property');
                continue;
              }
              walk(v, path + '.' + k);
            }
          }
          walk(schema, 'schema');

          if (jsonIssues.length > 0) {
            jsonIssues.forEach(m => console.log('::error::' + label + ': ' + m));
            console.log('::error::The server stores the schema as JSON — non-serializable members cause silent data loss.');
            process.exit(1);
          }

          console.log(label + ' is a valid JSON Schema object');
          process.exit(0);
          SCHEMACHECK
          node /tmp/check-schema.js

      - name: Test plugin stop()/start() lifecycle
        id: lifecycle-check
        shell: bash
        timeout-minutes: 2
        run: |
          cat << 'LIFECYCLE' > /tmp/check-lifecycle.js
          const path = require('path');
          const pkg = require(process.cwd() + '/package.json');
          // See validate-entry.js for the rationale — object/conditional
          // exports must be resolved, not flattened to 'index.js'.
          function resolveEntry(p, fallback) {
            if (p && p.main) return p.main;
            const e = p && p.exports;
            if (typeof e === 'string') return e;
            if (e && typeof e === 'object') {
              const root = typeof e['.'] !== 'undefined' ? e['.'] : e;
              if (typeof root === 'string') return root;
              if (root && typeof root === 'object') {
                for (const k of ['require', 'node', 'default', 'import']) {
                  if (typeof root[k] === 'string') return root[k];
                }
              }
            }
            return fallback;
          }
          const entry = resolveEntry(pkg, 'index.js');

          let mod;
          try {
            mod = require(path.resolve(entry));
          } catch (e) {
            console.log('Could not load plugin — skipping lifecycle check');
            process.exit(0);
          }

          const ctor = mod.default || mod;
          if (typeof ctor !== 'function') {
            process.exit(0);
          }

          // ── Delta validation helpers ──────────────────────────
          const deltaWarnings = [];
          const deltaErrors = [];
          // Each dot-separated segment must start with a letter and may contain
          // letters, digits, or hyphens — hyphens appear in plugin-chosen ids
          // (e.g. electrical.batteries.house-bank).
          const pathRe = /^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*)*$/;

          function validateDelta(id, data) {
            if (!data || typeof data !== 'object') {
              deltaErrors.push('handleMessage() called with non-object data: ' + typeof data);
              return;
            }
            if (!data.updates) {
              deltaErrors.push('handleMessage() called without updates field');
              return;
            }
            if (!Array.isArray(data.updates)) {
              deltaErrors.push('handleMessage() updates is ' + typeof data.updates + ' — must be an array');
              return;
            }
            if (data.updates.length === 0) {
              deltaWarnings.push('handleMessage() called with empty updates array');
              return;
            }
            for (const update of data.updates) {
              if (!update || typeof update !== 'object') {
                deltaErrors.push('updates[] contains non-object: ' + typeof update);
                continue;
              }
              const hasValues = 'values' in update;
              const hasMeta = 'meta' in update;
              if (!hasValues && !hasMeta) {
                deltaErrors.push('update has neither values nor meta — server will silently discard it');
              }
              if (hasValues && !Array.isArray(update.values)) {
                deltaErrors.push('update.values is ' + typeof update.values + ' — must be an array (server silently discards non-arrays)');
              }
              if (hasMeta && !Array.isArray(update.meta)) {
                deltaErrors.push('update.meta is ' + typeof update.meta + ' — must be an array');
              }
              const items = [
                ...(Array.isArray(update.values) ? update.values : []),
                ...(Array.isArray(update.meta) ? update.meta : [])
              ];
              for (const item of items) {
                if (!item || typeof item !== 'object') continue;
                if (typeof item.path !== 'string') {
                  deltaErrors.push('path is ' + typeof item.path + ' — must be a string');
                } else if (item.path !== '' && !pathRe.test(item.path)) {
                  deltaWarnings.push('path "' + item.path + '" has unusual format — expected dotted alphanumeric (e.g. navigation.position)');
                }
              }
            }
          }

          // ── Mock app with delta capture ─────────────────────────
          const mockApp = {
            // ── Server configuration ──────────────────────────────
            config: { configPath: '/tmp', version: '0.0.0' },
            selfType: 'uuid',
            selfId: 'urn:mrn:signalk:uuid:test',
            selfContext: 'vessels.urn:mrn:signalk:uuid:test',

            // ── Data model ────────────────────────────────────────
            getPath: (p) => undefined,
            getSelfPath: (p) => undefined,
            getMetadata: (p) => undefined,
            handleMessage: (id, data) => validateDelta(id, data),
            putSelfPath: (aPath, value, cb) => { if (cb) cb({ state: 'COMPLETED', statusCode: 200 }); return Promise.resolve(); },
            putPath: (aPath, value, cb) => { if (cb) cb({ state: 'COMPLETED', statusCode: 200 }); return Promise.resolve(); },
            queryRequest: () => Promise.resolve({}),
            streambundle: { getSelfBus: () => ({ onValue: () => ({ unsubscribe: () => {} }) }) },
            subscriptionmanager: { subscribe: () => ({ unsubscribe: () => {} }) },
            signalk: { self: 'vessels.urn:mrn:signalk:uuid:test', retrieve: () => ({}) },
            registerDeltaInputHandler: (handler) => {
              // Check the handler calls next() with a simple test
              let nextCalled = false;
              try {
                handler({ updates: [{ values: [{ path: 'test.ci.probe', value: 0 }] }] }, (d) => { nextCalled = true; });
              } catch (e) { /* handler may need real delta — skip check */ nextCalled = true; }
              if (!nextCalled) {
                deltaWarnings.push('registerDeltaInputHandler: handler did not call next() — this will silently drop deltas from all other plugins/sources');
              }
              return () => {};
            },

            // ── Plugin configuration ──────────────────────────────
            readPluginOptions: () => ({}),
            savePluginOptions: (configuration, cb) => { if (cb) cb(null); },
            getDataDirPath: () => '/tmp',

            // ── Status and debugging ──────────────────────────────
            debug: () => {},
            error: () => {},
            setPluginStatus: (msg) => console.log('  status: ' + msg),
            setPluginError: (msg) => console.log('  error: ' + msg),
            reportError: () => {},
            reportOutputMessages: () => {},

            // ── PUT / action handlers ─────────────────────────────
            registerPutHandler: () => {},
            registerActionHandler: () => {},

            // ── Event system ──────────────────────────────────────
            on: () => ({ unsubscribe: () => {} }),
            emit: () => {},
            emitPropertyValue: () => {},
            onPropertyValues: () => () => {},

            // ── Provider registrations (no-op in CI) ──────────────
            registerResourceProvider: () => {},
            registerAutopilotProvider: () => {},
            registerWeatherProvider: () => {},
            registerHistoryProvider: () => {},
            registerHistoryApiProvider: () => {},

            // ── API access (no-op stubs) ──────────────────────────
            getFeatures: () => Promise.resolve({ apis: [], plugins: [] }),
            getCourse: () => Promise.resolve({}),
            clearDestination: () => Promise.resolve(),
            setDestination: () => Promise.resolve(),
            activateRoute: () => Promise.resolve(),
            getSerialPorts: () => Promise.resolve({ byId: [], byPath: [], byOpenPlotter: [], serialports: [] }),
          };

          let plugin;
          try {
            plugin = ctor(mockApp);
          } catch (e) {
            console.log('Plugin constructor requires server context — skipping lifecycle check');
            process.exit(0);
          }

          if (!plugin || typeof plugin.start !== 'function' || typeof plugin.stop !== 'function') {
            console.log('Plugin does not have start()/stop() — skipping lifecycle check');
            process.exit(0);
          }

          // The server calls registerWithRouter() before start() if defined
          if (typeof plugin.registerWithRouter === 'function') {
            const noOp = () => mockRouter;
            const mockRouter = { get: noOp, post: noOp, put: noOp, delete: noOp, patch: noOp, use: noOp, all: noOp, route: noOp, param: noOp };
            try {
              plugin.registerWithRouter(mockRouter);
              console.log('registerWithRouter() — ok');
            } catch (e) {
              const msg = e.message || String(e);
              console.log('::warning::registerWithRouter() threw: ' + msg);
            }
          }

          // Detect errors caused by missing CI mock methods vs real plugin bugs
          function isMockGap(msg) {
            return /\w+ is not a function/.test(msg) || /Cannot read propert/.test(msg);
          }

          // Wrap in an async IIFE so that Promise-returning start()/stop()
          // hooks surface rejections via await — a sync try/catch around
          // `plugin.start({})` would silently swallow them.
          (async () => {
          try {
            await Promise.resolve(plugin.start({}));
            console.log('start({}) — ok');
          } catch (e) {
            const msg = e.message || String(e);
            console.log('::warning::plugin.start({}) threw: ' + msg);
            if (isMockGap(msg)) {
              console.log('::warning::This looks like a missing CI mock method, not a plugin bug. Please report at https://github.com/SignalK/signalk-server/issues');
            } else {
              console.log('::warning::Plugins should handle empty/default configuration gracefully.');
            }
            process.exit(0);
          }

          try {
            await Promise.resolve(plugin.stop());
            console.log('stop() — ok');
          } catch (e) {
            const msg = e.message || String(e);
            if (isMockGap(msg)) {
              console.log('::warning::plugin.stop() threw: ' + msg);
              console.log('::warning::This looks like a missing CI mock method, not a plugin bug. Please report at https://github.com/SignalK/signalk-server/issues');
              // Don't fail the build for mock gaps
            } else {
              console.log('::error::plugin.stop() threw: ' + msg);
              console.log('::error::This causes the server to leak resources when the plugin is disabled or restarted.');
              process.exit(1);
            }
          }

          // Restart: the server calls stop() then start() when config changes
          try {
            await Promise.resolve(plugin.start({}));
            console.log('start({}) again — ok (restart works)');
          } catch (e) {
            const msg = e.message || String(e);
            if (isMockGap(msg)) {
              console.log('::warning::plugin.start({}) threw on restart: ' + msg);
              console.log('::warning::This looks like a missing CI mock method, not a plugin bug. Please report at https://github.com/SignalK/signalk-server/issues');
              process.exit(0);
            }
            console.log('::error::plugin.start({}) threw on second call: ' + msg);
            console.log('::error::Plugins must support restart (stop then start). This fails when users toggle the plugin in the server UI.');
            process.exit(1);
          }

          try { await Promise.resolve(plugin.stop()); } catch (e) { /* ignore */ }

          // ── Report delta validation results ─────────────────────
          if (deltaWarnings.length > 0 || deltaErrors.length > 0) {
            console.log('');
            console.log('Delta validation (from deltas emitted during start):');
          }
          deltaWarnings.forEach(w => console.log('::warning::Delta: ' + w));
          deltaErrors.forEach(e => console.log('::error::Delta: ' + e));
          if (deltaErrors.length > 0) {
            console.log('');
            console.log('Malformed deltas are silently discarded by the server —');
            console.log('the plugin appears to work but data never reaches consumers.');
            process.exit(1);
          }

          console.log('Plugin lifecycle (start/stop/restart) is clean');
          process.exit(0);
          })().catch(e => {
            console.log('::error::Unexpected error during lifecycle check: ' + (e && e.message || String(e)));
            process.exit(1);
          });
          LIFECYCLE
          node /tmp/check-lifecycle.js

      - name: Scan for deprecated and misused SignalK APIs
        id: deprecated-api
        shell: bash
        run: |
          cat << 'APISCAN' > /tmp/check-api-usage.js
          const fs = require('fs');
          const path = require('path');

          // ── Deprecated APIs (warnings) ──────────────────────────
          const deprecated = [
            {
              pattern: /\bsetProviderStatus\s*\(/g,
              message: 'setProviderStatus() is deprecated — use setPluginStatus() instead',
            },
            {
              pattern: /\bsetProviderError\s*\(/g,
              message: 'setProviderError() is deprecated — use setPluginError() instead',
            },
          ];

          // ── API misuse (errors) ─────────────────────────────────
          // These access internal server properties not exposed to plugins
          const misuse = [
            {
              pattern: /\bapp\.server\b/g,
              message: 'app.server is an internal property — not part of the plugin API',
            },
            {
              pattern: /\bapp\.deltaCache\b/g,
              message: 'app.deltaCache is internal — use app.getPath() instead',
            },
            {
              pattern: /\bapp\.pluginsMap\b/g,
              message: 'app.pluginsMap is internal — not part of the plugin API',
            },
            {
              pattern: /\bhistoryApiHttpRegistry\b/g,
              message: 'historyApiHttpRegistry is internal (explicitly hidden from plugins) — use app.registerHistoryApiProvider()',
            },
          ];

          // ── Route registration anti-pattern (warnings) ──────────
          // Plugins should use registerWithRouter(router) not direct app.get/post
          const routeAntiPatterns = [
            {
              pattern: /\bapp\.(get|post|put|delete|patch|use)\s*\(\s*['"`\/]/g,
              message: 'Direct Express route on app object — use registerWithRouter(router) or signalKApiRoutes(router) instead',
            },
          ];

          // ── File storage anti-patterns (warnings) ───────────────
          // Plugins should use app.getDataDirPath() for data files,
          // not write to __dirname (node_modules), process.cwd() (server dir),
          // or access app.config.configPath directly
          const fileStorageAntiPatterns = [
            {
              pattern: /\b(writeFileSync|writeFile|mkdirSync|mkdir|appendFileSync|createWriteStream)\b[^)]*\b__dirname\b/g,
              message: 'Writing files relative to __dirname — this writes into node_modules. Use app.getDataDirPath() instead',
            },
            {
              pattern: /\b(writeFileSync|writeFile|mkdirSync|mkdir|appendFileSync|createWriteStream)\b[^)]*\bprocess\.cwd\(\)/g,
              message: 'Writing files relative to process.cwd() — this writes into the server directory. Use app.getDataDirPath() instead',
            },
            {
              pattern: /\bapp\.config\.configPath\b/g,
              message: 'Direct access to app.config.configPath — use app.getDataDirPath() for plugin data or app.savePluginOptions() for config',
            },
          ];

          // ── Security anti-patterns (warnings) ─────────────────────
          // Plugin routes via registerWithRouter() are automatically admin-protected.
          // PUT handlers via registerPutHandler() have built-in permission checks.
          // Plugins should NOT implement their own auth logic or access security internals.
          const securityAntiPatterns = [
            {
              pattern: /\bapp\.securityStrategy\b/g,
              message: 'Direct access to app.securityStrategy — this is a server internal. Plugin routes via registerWithRouter() are already admin-protected; PUT handlers via registerPutHandler() have built-in permission checks',
            },
            {
              pattern: /\bisDummy\s*\(\)/g,
              message: 'Checking isDummy() to branch security logic — plugins should not change behavior based on security mode. Use the built-in route protection instead',
            },
            {
              pattern: /\breq\.skPrincipal\b/g,
              message: 'Manual req.skPrincipal inspection — plugin routes under /plugins/* are already admin-only. If you need user identity, use req.skPrincipal.identifier but do not implement your own permission checks',
            },
            {
              pattern: /\baddAdminWriteMiddleware\b/g,
              message: 'addAdminWriteMiddleware is a server internal — plugin routes via registerWithRouter() are already admin-protected',
            },
            {
              pattern: /\baddWriteMiddleware\b/g,
              message: 'addWriteMiddleware is a server internal — use registerPutHandler() for write operations with built-in permission checks',
            },
          ];

          const srcFiles = [];
          function collectFiles(dir) {
            if (!fs.existsSync(dir)) return;
            for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
              if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') continue;
              const full = path.join(dir, entry.name);
              if (entry.isDirectory()) { collectFiles(full); continue; }
              if (/\.(js|ts|mjs|cjs)$/.test(entry.name)) srcFiles.push(full);
            }
          }
          collectFiles('.');

          const warnings = [];
          const errors = [];

          for (const file of srcFiles) {
            const content = fs.readFileSync(file, 'utf8');
            const lines = content.split('\n');
            const relFile = path.relative('.', file);

            // Strip comments before scanning so commented-out examples (either
            // "// app.server" or "/* app.server */" forms, on their own line
            // or inline with real code) do not trigger API misuse warnings.
            // String literals are not parsed — a comment-like sequence inside
            // a string is still treated as a comment, an accepted false-pos.
            const codeLines = new Array(lines.length);
            let inBlock = false;
            for (let i = 0; i < lines.length; i++) {
              const line = lines[i];
              let out = '';
              let pos = 0;
              while (pos < line.length) {
                if (inBlock) {
                  const end = line.indexOf('*/', pos);
                  if (end === -1) { pos = line.length; break; }
                  inBlock = false;
                  pos = end + 2;
                } else if (line.startsWith('//', pos)) {
                  break; // rest of line is a // comment
                } else if (line.startsWith('/*', pos)) {
                  inBlock = true;
                  pos += 2;
                } else {
                  out += line[pos];
                  pos += 1;
                }
              }
              codeLines[i] = out;
            }

            function scanPatterns(patterns, target) {
              for (const pat of patterns) {
                for (let i = 0; i < lines.length; i++) {
                  if (!codeLines[i]) continue;
                  if (pat.pattern.test(codeLines[i])) {
                    target.push(relFile + ':' + (i + 1) + ' — ' + pat.message);
                  }
                  pat.pattern.lastIndex = 0;
                }
              }
            }

            scanPatterns(deprecated, warnings);
            scanPatterns(misuse, errors);
            scanPatterns(routeAntiPatterns, warnings);
            scanPatterns(fileStorageAntiPatterns, warnings);
            scanPatterns(securityAntiPatterns, warnings);
          }

          // ── Node built-in module version checks ────────────────
          // Some node: built-in modules require specific Node versions.
          // If a plugin uses them but engines.node allows older versions, it will crash.
          const pkg = require(path.resolve('package.json'));
          const enginesNode = pkg.engines && pkg.engines.node;

          // Extract minimum Node version from engines.node (e.g. ">=18", ">=18.0.0", "^20", "18.x")
          function parseMinNodeVersion(range) {
            if (!range) return null;
            const m = range.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
            if (!m) return null;
            return [parseInt(m[1], 10), parseInt(m[2] || '0', 10), parseInt(m[3] || '0', 10)];
          }

          function versionLt(a, b) {
            // a, b are [major, minor, patch] arrays
            for (let i = 0; i < 3; i++) {
              if (a[i] < b[i]) return true;
              if (a[i] > b[i]) return false;
            }
            return false;
          }

          const nodeBuiltinVersionReqs = [
            { pattern: /\brequire\s*\(\s*['"]node:sqlite['"]\s*\)|from\s+['"]node:sqlite['"]/g, module: 'node:sqlite', minVersion: [22, 5, 0] },
            { pattern: /\brequire\s*\(\s*['"]node:test['"]\s*\)|from\s+['"]node:test['"]/g, module: 'node:test', minVersion: [18, 0, 0], testOnly: true },
          ];

          for (const file of srcFiles) {
            const content = fs.readFileSync(file, 'utf8');
            const relFile = path.relative('.', file);
            for (const check of nodeBuiltinVersionReqs) {
              check.pattern.lastIndex = 0;
              if (!check.pattern.test(content)) { check.pattern.lastIndex = 0; continue; }
              check.pattern.lastIndex = 0;

              // Skip test-only modules in test files
              if (check.testOnly && /\b(test|spec|__tests__)\b/i.test(relFile)) continue;

              // If the file has try/catch or .catch() around the import, it handles
              // the module being unavailable gracefully (e.g. dynamic import with fallback).
              // Downgrade to warning instead of error.
              const hasTryCatch = /try\s*\{[\s\S]*?\brequire\s*\(\s*['"]node:(sqlite|test)['"]\s*\)[\s\S]*?\}\s*catch/g.test(content) ||
                /import\s*\(\s*['"]node:(sqlite|test)['"]\s*\)/.test(content);

              const minVerStr = check.minVersion.join('.');

              if (!enginesNode) {
                // No engines.node at all — always an error. The plugin MUST declare
                // its minimum Node version so servers and users know the requirement.
                errors.push(relFile + ' — uses ' + check.module + ' (requires Node >= ' + minVerStr + ') but package.json has no engines.node field. Add engines.node to declare the minimum version.');
              } else {
                const minDeclared = parseMinNodeVersion(enginesNode);
                if (minDeclared && versionLt(minDeclared, check.minVersion)) {
                  // engines.node is set but allows older versions. If the plugin handles
                  // the module being unavailable (dynamic import with fallback), downgrade
                  // to warning — the plugin works in degraded mode on older Node.
                  const target = hasTryCatch ? warnings : errors;
                  const suffix = hasTryCatch ? ' (dynamic import with fallback detected — verify graceful degradation)' : '';
                  target.push(relFile + ' — uses ' + check.module + ' (requires Node >= ' + minVerStr + ') but engines.node "' + enginesNode + '" allows Node ' + minDeclared.join('.') + suffix);
                }
              }
            }
          }

          if (warnings.length === 0 && errors.length === 0) {
            console.log('No deprecated or misused API patterns found');
          }
          warnings.forEach(w => console.log('::warning::' + w));
          errors.forEach(e => console.log('::error::' + e));

          if (errors.length > 0) {
            console.log('');
            console.log('Found ' + errors.length + ' API misuse(s). These access internal server');
            console.log('properties that are not part of the plugin API and may break');
            console.log('without notice in future server updates.');
            console.log('');
            console.log('See: https://signalk.org/signalk-server/develop/plugins/server_api.html');
            process.exit(1);
          }
          process.exit(0);
          APISCAN
          node /tmp/check-api-usage.js

      - name: Verify npm pack includes all required files
        id: pack-check
        shell: bash
        timeout-minutes: 2
        run: |
          cat << 'PACKCHECK' > /tmp/check-pack.js
          const { execSync } = require('child_process');
          const pkg = require(process.cwd() + '/package.json');
          const path = require('path');

          // See validate-entry.js for the rationale. Null fallback: if the
          // plugin declares no main/exports, there is nothing to check.
          function resolveEntry(p, fallback) {
            if (p && p.main) return p.main;
            const e = p && p.exports;
            if (typeof e === 'string') return e;
            if (e && typeof e === 'object') {
              const root = typeof e['.'] !== 'undefined' ? e['.'] : e;
              if (typeof root === 'string') return root;
              if (root && typeof root === 'object') {
                for (const k of ['require', 'node', 'default', 'import']) {
                  if (typeof root[k] === 'string') return root[k];
                }
              }
            }
            return fallback;
          }

          // Get list of files that npm pack would include
          // --ignore-scripts: don't re-run prepack (we already built in a prior step)
          // stdio pipe suppresses stderr cross-platform (2>/dev/null breaks on Windows)
          let packOutput;
          try {
            packOutput = execSync('npm pack --dry-run --json --ignore-scripts', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
          } catch (e) {
            packOutput = e.stdout || '';
          }
          let files;
          try {
            const parsed = JSON.parse(packOutput);
            files = parsed[0].files.map(f => f.path);
          } catch (e) {
            // Fallback: parse non-JSON dry-run output
            let raw;
            try {
              raw = execSync('npm pack --dry-run --ignore-scripts', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
            } catch (e2) {
              raw = e2.stdout || '';
            }
            files = raw.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('npm') && !l.startsWith('Tarball'));
          }

          const errors = [];
          const warnings = [];

          // Check main entry point is included
          const main = resolveEntry(pkg, null);
          if (main && !files.includes(main) && !files.includes(main.replace(/^\.\//, ''))) {
            errors.push('"main" field points to ' + main + ' but this file is NOT in the npm package.');
            errors.push('The plugin will be broken when installed from npm. Add it to "files" in package.json or remove it from .npmignore.');
          }

          // Check exports map entries
          if (pkg.exports && typeof pkg.exports === 'object') {
            function checkExports(obj, prefix) {
              for (const [key, val] of Object.entries(obj)) {
                if (typeof val === 'string') {
                  const clean = val.replace(/^\.\//, '');
                  if (!files.includes(clean) && !files.includes(val)) {
                    errors.push('"exports" entry ' + prefix + key + ' points to ' + val + ' but it is NOT in the npm package.');
                  }
                } else if (typeof val === 'object') {
                  checkExports(val, prefix + key + '.');
                }
              }
            }
            checkExports(pkg.exports, '');
          }

          // Check for schema.json if plugin defines schema()
          if (files.some(f => /schema\.json/i.test(f))) {
            console.log('schema.json found in package — good');
          }

          // Warn if dist/ or lib/ referenced but not included
          if (main && (main.startsWith('dist/') || main.startsWith('lib/'))) {
            const dir = main.split('/')[0];
            if (!files.some(f => f.startsWith(dir + '/'))) {
              errors.push('"main" references ' + dir + '/ directory but no files from it are in the package. Did you forget to build, or need to add "' + dir + '" to the "files" array in package.json?');
            }
          }

          warnings.forEach(w => console.log('::warning::' + w));
          if (errors.length > 0) {
            errors.forEach(e => console.log('::error::' + e));
            process.exit(1);
          }
          console.log('All entry points are included in the npm package');
          process.exit(0);
          PACKCHECK
          node /tmp/check-pack.js

      - name: Check ES2023 compatibility (Cerbo GX / Node 20)
        id: es-check
        if: always() && steps.pack-check.outcome != 'skipped'
        shell: bash
        run: |
          # Cerbo GX (Venus OS) runs Node 20 which supports ES2023 syntax.
          # Plugins using ES2024+ syntax (RegExp v flag, import attributes)
          # will crash with SyntaxError on Node 20.
          # Only check server-side JS (dist/, lib/, main entry) — skip public/
          # which is frontend code running in browsers.
          FILES=""
          for dir in dist lib; do
            if [ -d "$dir" ]; then
              for ext in js cjs mjs; do
                if find "$dir" -name "*.$ext" -print -quit 2>/dev/null | grep -q .; then
                  FILES="$FILES $dir/**/*.$ext"
                fi
              done
            fi
          done

          MAIN=$(node -e "
            const pkg = require('./package.json');
            function resolveEntry(p, fallback) {
              if (p && p.main) return p.main;
              const e = p && p.exports;
              if (typeof e === 'string') return e;
              if (e && typeof e === 'object') {
                const root = typeof e['.'] !== 'undefined' ? e['.'] : e;
                if (typeof root === 'string') return root;
                if (root && typeof root === 'object') {
                  for (const k of ['require', 'node', 'default', 'import']) {
                    if (typeof root[k] === 'string') return root[k];
                  }
                }
              }
              return fallback;
            }
            const main = resolveEntry(pkg, null);
            if (main) console.log(main);
          " 2>/dev/null)
          if [ -n "$MAIN" ] && [ -f "$MAIN" ] && [ -z "$FILES" ]; then
            FILES="$MAIN"
          fi

          if [ -z "$FILES" ]; then
            echo "No built JS files found to check"
            exit 0
          fi

          echo "Checking ES2023 compatibility: $FILES"
          if ! npx --yes es-check@8 es2023 $FILES 2>&1; then
            echo ""
            echo "::warning::Plugin uses ES2024+ syntax not supported by Node 20 (Cerbo GX / Venus OS). Consider targeting ES2023 in your TypeScript/build config or tsconfig.json to maintain Cerbo GX compatibility."
          fi

      - name: Simulate App Store install (--ignore-scripts)
        id: appstore-check
        shell: bash
        env:
          APPSTORE_DIR: ${{ runner.temp }}/appstore-test
        run: |
          # SignalK server installs plugins with --ignore-scripts (src/modules.ts:223)
          # This means native addons (better-sqlite3, serialport, etc.) are NOT compiled.
          # This test catches plugins that will break when installed via the App Store.
          echo "Simulating SignalK App Store install (npm install --ignore-scripts)..."
          PLUGIN_TGZ=$(npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP")

          mkdir -p "$APPSTORE_DIR"
          cd "$APPSTORE_DIR"
          npm init -y > /dev/null 2>&1
          npm install --ignore-scripts "$RUNNER_TEMP/$(basename "$PLUGIN_TGZ")" 2>&1

          # Scan for native addon dependencies (binding.gyp = needs compilation)
          # Uses Node.js for cross-platform compatibility (works on Linux, macOS, Windows)
          cat << 'CHECKSCRIPT' > "$RUNNER_TEMP/check-native.js"
          const fs = require('fs');
          const path = require('path');
          const nmDir = path.join(process.env.APPSTORE_DIR, 'node_modules');
          const nativeModules = [];

          function findNativeModules(dir) {
            if (!fs.existsSync(dir)) return;
            for (const entry of fs.readdirSync(dir)) {
              if (entry === '.package-lock.json') continue;
              const full = path.join(dir, entry);
              if (entry.startsWith('@')) {
                findNativeModules(full);
                continue;
              }
              if (fs.existsSync(path.join(full, 'binding.gyp'))) {
                nativeModules.push(entry);
              }
              const nested = path.join(full, 'node_modules');
              if (fs.existsSync(nested)) findNativeModules(nested);
            }
          }

          findNativeModules(nmDir);

          if (nativeModules.length === 0) {
            console.log('No native addons found — App Store install is safe');
            process.exit(0);
          }

          // Classify native modules as required vs optional using the lockfile
          const optionalSet = new Set();
          const lockPath = path.join(nmDir, '.package-lock.json');
          if (fs.existsSync(lockPath)) {
            try {
              const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
              for (const [key, meta] of Object.entries(lock.packages || {})) {
                if (meta.optional) optionalSet.add(path.basename(key));
              }
            } catch (e) { /* ignore parse errors — treat all as required */ }
          }

          const requiredNative = nativeModules.filter(m => !optionalSet.has(m));
          const optionalNative = nativeModules.filter(m => optionalSet.has(m));

          if (optionalNative.length > 0) {
            console.log('::warning::Optional native addons found: ' + optionalNative.join(', ') + '. App Store uses --ignore-scripts so these will not be compiled. The parent package declares them optional, so the plugin may still work — verify it works without them.');
          }

          if (requiredNative.length > 0) {
            console.log('::error::Plugin depends on native addons: ' + requiredNative.join(', ') + '. App Store uses --ignore-scripts — native addons will NOT be compiled and the plugin will CRASH at runtime.');
            console.log('');
            console.log('Use alternatives that do not require native compilation, e.g.:');
            console.log('  - node:sqlite (built into Node >= 22) instead of better-sqlite3');
            console.log('  - Pure JavaScript packages instead of native addons');
            process.exit(1);
          }

          process.exit(0);
          CHECKSCRIPT
          node "$RUNNER_TEMP/check-native.js"

      - name: Lint (if available)
        run: npm run lint --if-present
        continue-on-error: true

      - name: Check formatting
        if: ${{ inputs.format-check-command != '' }}
        env:
          FORMAT_CMD: ${{ inputs.format-check-command }}
        shell: bash
        run: eval "$FORMAT_CMD"

      - name: Run tests with coverage
        if: ${{ inputs.coverage-command != '' }}
        id: coverage
        env:
          COVERAGE_CMD: ${{ inputs.coverage-command }}
        shell: bash
        run: |
          set -eo pipefail
          eval "$COVERAGE_CMD" | tee "$RUNNER_TEMP/coverage.log"
          {
            echo '## Test coverage (Node ${{ matrix.node }} / ${{ runner.os }})'
            echo
            echo '```'
            cat "$RUNNER_TEMP/coverage.log"
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Run tests
        if: ${{ inputs.coverage-command == '' }}
        id: tests
        env:
          TEST_CMD: ${{ inputs.test-command }}
        shell: bash
        run: |
          # Detect if the plugin has a real test script
          HAS_TESTS=$(node -e "
            const pkg = require('./package.json');
            const test = pkg.scripts && pkg.scripts.test;
            // npm init sets test to 'echo \"Error: no test specified\" && exit 1'
            if (!test || test.includes('no test specified')) {
              process.exit(1);
            }
          " && echo "yes" || echo "no")

          if [ "$HAS_TESTS" = "yes" ]; then
            eval "$TEST_CMD"
          else
            echo "::notice::No test script defined — skipping tests"
          fi

      - name: Check for untracked files after build/test
        id: stray-files
        shell: bash
        run: |
          # Plugins should not leave stray files in the repo after install/build/test.
          UNTRACKED=$(git ls-files --others --exclude-standard -- ':!node_modules' ':!.npm')
          if [ -n "$UNTRACKED" ]; then
            echo "::warning::Build/test left untracked files in the repository:"
            echo "$UNTRACKED" | while read -r f; do echo "  $f"; done
            echo ""
            echo "These files would pollute the signalk-server git directory when"
            echo "the plugin is installed. Add them to .gitignore or fix the build"
            echo "to write output into node_modules or a temp directory."
          fi

      - name: Write job summary
        if: always()
        shell: bash
        env:
          VALIDATE_PKG: ${{ steps.validate-pkg.outcome }}
          VALIDATE_ENTRY: ${{ steps.validate-entry.outcome }}
          VALIDATE_SCHEMA: ${{ steps.validate-schema.outcome }}
          LIFECYCLE_CHECK: ${{ steps.lifecycle-check.outcome }}
          DEPRECATED_API: ${{ steps.deprecated-api.outcome }}
          PACK_CHECK: ${{ steps.pack-check.outcome }}
          APPSTORE_CHECK: ${{ steps.appstore-check.outcome }}
          # When coverage-command is set, steps.tests is skipped and the
          # coverage step runs instead — surface whichever actually ran.
          TESTS: ${{ inputs.coverage-command != '' && steps.coverage.outcome || steps.tests.outcome }}
          STRAY_FILES: ${{ steps.stray-files.outcome }}
          ES_CHECK: ${{ steps.es-check.outcome }}
          OS_NAME: >-
            ${{ matrix.os == 'ubuntu-latest' && 'Linux' ||
                matrix.os == 'ubuntu-24.04-arm' && 'Linux arm64' ||
                matrix.os == 'macos-latest' && 'macOS' ||
                matrix.os == 'windows-latest' && 'Windows' ||
                matrix.os }}
          NODE_VER: ${{ matrix.node }}
        run: |
          icon() {
            case "$1" in
              success)  echo "pass" ;;
              failure)  echo "FAIL" ;;
              skipped)  echo "skip" ;;
              *)        echo "—" ;;
            esac
          }

          PLUGIN_NAME=$(node -e "console.log(require('./package.json').name)" 2>/dev/null || echo "unknown")
          PLUGIN_VER=$(node -e "console.log(require('./package.json').version)" 2>/dev/null || echo "?")

          {
            echo "### $OS_NAME / Node $NODE_VER — $PLUGIN_NAME@$PLUGIN_VER"
            echo ""
            echo "| Check | Result |"
            echo "|-------|--------|"
            echo "| package.json valid | $(icon "$VALIDATE_PKG") |"
            echo "| Entry point exports function | $(icon "$VALIDATE_ENTRY") |"
            echo "| plugin.schema() valid | $(icon "$VALIDATE_SCHEMA") |"
            echo "| start/stop/restart lifecycle | $(icon "$LIFECYCLE_CHECK") |"
            echo "| API usage (no deprecated/internal) | $(icon "$DEPRECATED_API") |"
            echo "| npm pack includes all files | $(icon "$PACK_CHECK") |"
            echo "| App Store compatible (no native addons) | $(icon "$APPSTORE_CHECK") |"
            echo "| Tests / coverage | $(icon "$TESTS") |"
            echo "| ES2023 compatible (Cerbo GX / Node 20) | $(icon "$ES_CHECK") |"
            echo "| No stray files after build | $(icon "$STRAY_FILES") |"
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Upload test artifacts
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: test-results-${{ matrix.os }}-node${{ matrix.node }}
          path: |
            test-results/
            coverage/
            junit*.xml
          if-no-files-found: ignore
          retention-days: 7

  # ════════════════════════════════════════════════════════════
  #  armv7 — Cerbo GX / Raspberry Pi (32-bit ARM via QEMU)
  # ════════════════════════════════════════════════════════════
  armv7:
    name: armv7 (Cerbo GX) / Node 20
    runs-on: ubuntu-latest
    timeout-minutes: 30
    if: ${{ inputs.enable-armv7 }}
    # Expose the test-step outcome so ci-status can distinguish a step
    # failure (advisory) from the job itself completing.
    outputs:
      advisory-outcome: ${{ steps.armv7-test.outcome }}
    steps:
      - name: Checkout plugin
        uses: actions/checkout@v6

      - name: Set up QEMU for ARM emulation
        # Advisory: a QEMU setup failure must not block the workflow — the
        # armv7 test step handles its own failure via continue-on-error, and
        # if QEMU truly didn't initialize, docker run will fail there instead.
        continue-on-error: true
        uses: docker/setup-qemu-action@v4
        with:
          platforms: linux/arm/v7

      - name: Cache npm for armv7
        uses: actions/cache@v5
        with:
          path: /tmp/.npm-armv7-cache
          key: armv7-npm-${{ hashFiles('**/package-lock.json', '**/package.json') }}
          restore-keys: |
            armv7-npm-

      - name: Test on armv7 via QEMU
        id: armv7-test
        # Advisory: do not fail the workflow when armv7 tests fail. The
        # outcome is captured via the job output above and surfaced in the
        # ci-status summary.
        continue-on-error: true
        env:
          BUILD_CMD: ${{ inputs.build-command }}
          TEST_CMD: ${{ inputs.test-command }}
        run: |
          docker run --rm --platform linux/arm/v7 \
            -v "${{ github.workspace }}:/plugin" \
            -v "/tmp/.npm-armv7-cache:/root/.npm" \
            -w /plugin \
            -e BUILD_CMD \
            -e TEST_CMD \
            node:20-bookworm-slim \
            bash -c '
              set -e
              echo "── System info ──"
              echo "arch: $(uname -m)  node: $(node -v)  npm: $(npm -v)"
              echo "── Installing build tools (python3, make, g++) ──"
              apt-get update -qq && apt-get install -y -qq python3 make g++ > /dev/null 2>&1
              echo "── Installing dependencies ──"
              if [ -f package-lock.json ]; then
                npm ci --ignore-scripts
              else
                npm install --ignore-scripts
              fi
              npm rebuild
              echo "── Building ──"
              eval "$BUILD_CMD"

              echo "── Checking for native addons ──"
              node -e "
                const fs = require(\"fs\"), path = require(\"path\");
                const nm = [];
                function scan(dir) {
                  if (!fs.existsSync(dir)) return;
                  for (const e of fs.readdirSync(dir)) {
                    if (e === \".package-lock.json\") continue;
                    const f = path.join(dir, e);
                    if (e.startsWith(\"@\")) { scan(f); continue; }
                    if (fs.existsSync(path.join(f, \"binding.gyp\"))) nm.push(e);
                    const n = path.join(f, \"node_modules\");
                    if (fs.existsSync(n)) scan(n);
                  }
                }
                scan(\"./node_modules\");
                if (nm.length === 0) { console.log(\"No native addons\"); process.exit(0); }
                console.log(\"Native addons compiled for armv7: \" + nm.join(\", \"));
              "

              echo "── Running tests ──"
              HAS_TESTS=$(node -e "
                const pkg = require(\"./package.json\");
                const test = pkg.scripts && pkg.scripts.test;
                if (!test || test.includes(\"no test specified\")) process.exit(1);
              " && echo "yes" || echo "no")
              if [ "$HAS_TESTS" = "yes" ]; then
                eval "$TEST_CMD"
              else
                echo "::notice::No test script defined — skipping tests"
              fi
            '

      - name: Write job summary
        if: always()
        shell: bash
        run: |
          PLUGIN_NAME=$(node -e "console.log(require('./package.json').name)" 2>/dev/null || echo "unknown")
          PLUGIN_VER=$(node -e "console.log(require('./package.json').version)" 2>/dev/null || echo "?")
          # Use the test-step outcome, not job.status — the job itself
          # succeeds (continue-on-error) even when the step fails.
          RESULT="${{ steps.armv7-test.outcome }}"

          {
            echo "### armv7 (Cerbo GX) / Node 20 — ${PLUGIN_NAME}@${PLUGIN_VER}"
            echo ""
            echo "**Environment**: Venus OS 3.70 emulation (QEMU armv7, Debian Bookworm, python3, make, g++)"
            echo ""
            if [[ "$RESULT" == "success" ]]; then
              echo "Install, build, and tests completed successfully under armv7 emulation."
            else
              echo "The armv7 job **failed**. Your plugin may not work on 32-bit ARM devices (Cerbo GX, Raspberry Pi OS 32-bit)."
            fi
            echo ""
            echo "> This test is **advisory and non-blocking** — armv7 failures do not fail the overall CI."
          } >> "$GITHUB_STEP_SUMMARY"

  # ════════════════════════════════════════════════════════════
  #  Optional: SignalK server integration test
  # ════════════════════════════════════════════════════════════
  signalk-integration:
    name: Integration / signalk-server ${{ matrix.signalk-server-version }} / Node ${{ matrix.node }}
    runs-on: ubuntu-latest
    timeout-minutes: 15
    if: ${{ inputs.enable-signalk-integration }}
    strategy:
      fail-fast: false
      matrix:
        node: ${{ fromJson(inputs.node-versions) }}
        signalk-server-version: ${{ fromJson(inputs.signalk-server-versions) }}
    steps:
      - name: Checkout plugin
        uses: actions/checkout@v6

      - name: Setup Node.js ${{ matrix.node }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node }}
          cache: ${{ hashFiles('**/package-lock.json') != '' && 'npm' || '' }}

      - name: Install SignalK server ${{ matrix.signalk-server-version }}
        run: |
          mkdir -p /tmp/sk-test
          cd /tmp/sk-test
          npm init -y
          npm install signalk-server@${{ matrix.signalk-server-version }}

      - name: Build plugin
        env:
          BUILD_CMD: ${{ inputs.build-command }}
        shell: bash
        run: |
          if [ -f package-lock.json ]; then
            npm ci
          else
            npm install
          fi
          eval "$BUILD_CMD"

      - name: Install plugin into SignalK
        run: |
          # Pack the plugin and install it into the test server. Use basename
          # defensively: some npm versions emit a path-prefixed tarball name.
          PLUGIN_TGZ=$(npm pack --ignore-scripts --pack-destination /tmp)
          cd /tmp/sk-test
          npm install "/tmp/$(basename "$PLUGIN_TGZ")"

      - name: Configure plugin to auto-start
        run: |
          cd /tmp/sk-test
          PLUGIN_PKG_NAME=$(node -e "console.log(require('${{ github.workspace }}/package.json').name)")

          # Derive plugin ID the same way signalk-server does (src/pluginid.ts)
          PLUGIN_ID=$(node -e "
            const name = process.argv[1];
            console.log(name.replace(/@/g, '_').replace(/\//g, '_'));
          " "$PLUGIN_PKG_NAME")

          echo "Plugin package: $PLUGIN_PKG_NAME"
          echo "Plugin ID:      $PLUGIN_ID"

          mkdir -p plugin-config-data
          echo '{"enabled": true, "configuration": {}}' > "plugin-config-data/${PLUGIN_ID}.json"

      - name: Start SignalK server and verify plugin
        env:
          SIGNALK_URL: 'http://localhost:3000'
        run: |
          cd /tmp/sk-test
          export SIGNALK_NODE_CONFIG_DIR=/tmp/sk-test

          # Start server with both NMEA 0183 and N2K sample data so plugins
          # have a rich data environment (navigation, wind, depth, temperature, etc.)
          # --override-timestamps keeps N2K data current instead of stale
          npx signalk-server --sample-nmea0183-data --sample-n2k-data --override-timestamps -p 3000 &
          SK_PID=$!
          # Ensure the background server is killed on any exit path — step
          # failure, early return from a check, or normal completion. Without
          # this a failing `npm run test:integration` would leak the process.
          trap 'kill $SK_PID 2>/dev/null || true' EXIT

          # Wait for server to be ready (max 90 seconds)
          echo "Waiting for SignalK server..."
          READY=false
          for i in $(seq 1 45); do
            if curl -sf http://localhost:3000/signalk/v1/api/ > /dev/null 2>&1; then
              READY=true
              break
            fi
            sleep 2
          done

          if [ "$READY" != "true" ]; then
            echo "::error::SignalK server failed to start within 90 seconds"
            exit 1
          fi
          echo "SignalK server is ready (PID $SK_PID)"

          # Verify plugin is loaded
          PLUGIN_PKG_NAME=$(node -e "console.log(require('${{ github.workspace }}/package.json').name)")
          PLUGINS_JSON=$(curl -sf http://localhost:3000/skServer/plugins)

          echo "Loaded plugins:"
          echo "$PLUGINS_JSON" | jq -r '.[] | "  - \(.packageName // .id) (\(.state // "unknown"))"'

          # Assert our plugin is in the list
          if ! echo "$PLUGINS_JSON" | jq -e ".[] | select(.packageName == \"$PLUGIN_PKG_NAME\")" > /dev/null 2>&1; then
            echo "::error::Plugin '$PLUGIN_PKG_NAME' not found in loaded plugins"
            echo "$PLUGINS_JSON" | jq .
            exit 1
          fi
          echo "Plugin '$PLUGIN_PKG_NAME' is loaded"

          # Verify provider API registrations
          # If the plugin registers as a provider (history, resources, etc.),
          # check that the corresponding server endpoints actually respond.
          # This catches the case where a plugin calls registerHistoryApiProvider()
          # but the endpoint still returns 501 (broken registration), or where a
          # webapp/plugin depends on an API that no installed provider serves.
          echo ""
          echo "Checking provider API registrations..."

          PROVIDER_ERRORS=0

          # History API v2 — check if plugin registered as a history provider
          HISTORY_PROVIDERS=$(curl -sf http://localhost:3000/signalk/v2/api/history/_providers || echo "{}")
          if echo "$HISTORY_PROVIDERS" | jq -e 'keys | length > 0' > /dev/null 2>&1; then
            echo "History API v2: provider(s) registered"
            echo "$HISTORY_PROVIDERS" | jq -r 'to_entries[] | "  - \(.key)\(if .value.isDefault then " (default)" else "" end)"'

            # Verify the values endpoint responds (not 501)
            FROM=$(date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)
            TO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
            # No -f: we need to capture 4xx/5xx codes (notably 501) instead of
            # exiting early with an empty HISTORY_STATUS.
            HISTORY_STATUS=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000/signalk/v2/api/history/values?paths=navigation.position&from=${FROM}&to=${TO}&resolution=60" || echo "000")
            if [ "$HISTORY_STATUS" = "501" ]; then
              echo "::error::History API v2: provider registered but /history/values returns 501 (no provider configured)"
              PROVIDER_ERRORS=$((PROVIDER_ERRORS + 1))
            elif [ "$HISTORY_STATUS" = "200" ]; then
              echo "History API v2: /history/values responds OK"
            else
              echo "::warning::History API v2: /history/values returned HTTP $HISTORY_STATUS"
            fi
          else
            echo "History API v2: no providers registered (OK — plugin does not provide history)"
          fi

          if [ "$PROVIDER_ERRORS" -gt 0 ]; then
            echo ""
            echo "::error::Provider API verification found $PROVIDER_ERRORS error(s)"
            exit 1
          fi

          # Run integration tests from the plugin repo if they exist
          cd "${{ github.workspace }}"
          HAS_INTEGRATION=$(node -e "
            const pkg = require('./package.json');
            if (pkg.scripts && pkg.scripts['test:integration']) process.exit(0);
            process.exit(1);
          " && echo "yes" || echo "no")

          if [ "$HAS_INTEGRATION" = "yes" ]; then
            echo "Running integration tests..."
            SIGNALK_URL=http://localhost:3000 npm run test:integration
          else
            echo "::notice::No test:integration script found — skipping"
          fi
          # Server cleanup is handled by the EXIT trap set above.

  # ════════════════════════════════════════════════════════════
  #  Summary — collect all results
  # ════════════════════════════════════════════════════════════
  ci-status:
    name: CI Status
    runs-on: ubuntu-latest
    timeout-minutes: 5
    if: always()
    needs: [desktop, armv7, signalk-integration]
    steps:
      - name: Checkout plugin
        uses: actions/checkout@v6
        with:
          sparse-checkout: package.json

      - name: Write CI summary report
        env:
          DESKTOP_RESULT: ${{ needs.desktop.result }}
          # Prefer the armv7 test-step outcome when the job ran — it records
          # whether the advisory tests actually passed. Falls back to
          # needs.armv7.result when the job was skipped/cancelled and no
          # step outcome exists.
          ARMV7_RESULT: ${{ needs.armv7.outputs.advisory-outcome || needs.armv7.result }}
          INTEGRATION_RESULT: ${{ needs.signalk-integration.result }}
        shell: bash
        run: |
          icon() {
            case "$1" in
              success)  echo "pass" ;;
              failure)  echo "FAIL" ;;
              skipped)  echo "skip" ;;
              cancelled) echo "cancel" ;;
              *)        echo "—" ;;
            esac
          }

          PLUGIN_NAME=$(node -e "console.log(require('./package.json').name)" 2>/dev/null || echo "unknown")
          PLUGIN_VER=$(node -e "console.log(require('./package.json').version)" 2>/dev/null || echo "?")

          OVERALL="pass"
          if [[ "$DESKTOP_RESULT" == "failure" ]] || [[ "$INTEGRATION_RESULT" == "failure" ]]; then
            OVERALL="FAIL"
          fi

          {
            echo "## SignalK Plugin CI Report"
            echo ""
            echo "**Plugin**: \`$PLUGIN_NAME@$PLUGIN_VER\`"
            echo "**Result**: $OVERALL"
            echo ""
            echo "### Platform results"
            echo ""
            echo "| Job | Result | Notes |"
            echo "|-----|--------|-------|"
            echo "| Desktop (Linux, Linux arm64, macOS, Windows) | $(icon "$DESKTOP_RESULT") | Node 22, 24 |"
            echo "| armv7 — Cerbo GX (QEMU) | $(icon "$ARMV7_RESULT") | Node 20 (Venus OS 3.70) — advisory, non-blocking |"
            if [[ "$INTEGRATION_RESULT" != "skipped" ]]; then
              echo "| SignalK integration | $(icon "$INTEGRATION_RESULT") | Server install + plugin load |"
            fi
            echo ""
            echo "### What was checked"
            echo ""
            echo "Every desktop job validates:"
            echo "- **package.json** — \`signalk-node-server-plugin\` keyword, \`main\`/\`exports\` field, \`engines.node\`"
            echo "- **Entry point** — Plugin exports a constructor function"
            echo "- **plugin.schema()** — Returns valid JSON Schema without crashing"
            echo "- **Lifecycle** — start()/stop()/restart cycle works without errors"
            echo "- **API usage** — No deprecated (\`setProviderStatus\`), internal (\`app.server\`), file storage, or security anti-patterns"
            echo "- **Node built-in modules** — \`node:sqlite\` requires \`engines.node >= 22.5.0\` declared in package.json"
            echo "- **npm pack** — All files referenced by \`main\`/\`exports\` are included in the package"
            echo "- **App Store install** — No native addons that break with \`--ignore-scripts\`"
            echo "- **Hardcoded paths** — No \`/home/user/...\` literals in source files"
            echo "- **ES2023 compatibility** — Built JS checked for Cerbo GX (Node 20) syntax compatibility"
            echo "- **baconjs** — Plugins must not bundle their own baconjs; the server provides >= 3.x"
            echo "- **React version** — Plugins with webapp keywords must use React >= 19 for Module Federation compatibility"
            echo "- **Stray files** — Build/test does not leave untracked files in the repo"
            echo ""
            echo "See the individual job summaries above for per-platform details."
          } >> "$GITHUB_STEP_SUMMARY"

          if [[ "$INTEGRATION_RESULT" == "skipped" ]]; then
            {
              echo ""
              echo "> **Tip**: You can also test your plugin against a running Signal K server."
              echo "> Add \`enable-signalk-integration: true\` to your workflow. [Learn more](https://signalk.org/signalk-server/develop/plugins/ci.html#integration-tests)"
            } >> "$GITHUB_STEP_SUMMARY"
          fi

          if [[ "$ARMV7_RESULT" == "failure" ]]; then
            {
              echo ""
              echo "> **Note**: armv7 (Cerbo GX) failed but this is non-blocking."
              echo "> Your plugin may not work on 32-bit ARM devices (Cerbo GX, Raspberry Pi OS 32-bit)."
            } >> "$GITHUB_STEP_SUMMARY"
          fi

          echo ""
          echo "Desktop:              $DESKTOP_RESULT"
          echo "armv7 (Cerbo GX):     $ARMV7_RESULT"
          echo "SignalK integration:  $INTEGRATION_RESULT"

          # Desktop is mandatory
          if [[ "$DESKTOP_RESULT" == "failure" ]]; then
            echo "::error::Desktop tests failed"
            exit 1
          fi

          # armv7 is advisory (non-blocking)
          case "$ARMV7_RESULT" in
            failure) echo "::warning::armv7 (Cerbo GX) tests failed — plugin may not work on 32-bit ARM devices" ;;
            skipped) echo "armv7 tests were skipped" ;;
          esac

          # Integration is mandatory if enabled
          if [[ "$INTEGRATION_RESULT" == "failure" ]]; then
            echo "::error::SignalK integration tests failed"
            exit 1
          fi

          echo "All checks passed!"


================================================
FILE: .github/workflows/release.yml
================================================
name: Release - build & publish modules and server, build & publish docker containers

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

permissions:
  id-token: write # Required for OIDC
  contents: read

jobs:
  build_and_publish:
    if: github.repository == 'SignalK/signalk-server'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: '24.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Install and build all
        run: |
          npm cache clean -f
          npm install npm@latest -g
          npm install --package-lock-only
          npm ci && npm cache clean --force
          npm run build:all

      - name: Publish server-admin-ui-dependencies
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/server-admin-ui-dependencies/package.json)
          if ! npm view @signalk/server-admin-ui-dependencies@$LOCAL_VERSION version &>/dev/null; then
            cd packages/server-admin-ui-dependencies
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish server-admin-ui
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/server-admin-ui/package.json)
          if ! npm view @signalk/server-admin-ui@$LOCAL_VERSION version &>/dev/null; then
            cd packages/server-admin-ui
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish server-api
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/server-api/package.json)
          if ! npm view @signalk/server-api@$LOCAL_VERSION version &>/dev/null; then
            cd packages/server-api
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish streams
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/streams/package.json)
          if ! npm view @signalk/streams@$LOCAL_VERSION version &>/dev/null; then
            cd packages/streams
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish resources-provider
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/resources-provider-plugin/package.json)
          if ! npm view @signalk/resources-provider@$LOCAL_VERSION version &>/dev/null; then
            cd packages/resources-provider-plugin
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish typedoc-signalk-theme
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/typedoc-theme/package.json)
          if ! npm view @signalk/typedoc-signalk-theme@$LOCAL_VERSION version &>/dev/null; then
            cd packages/typedoc-theme
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish assemblyscript-plugin-sdk
        run: |
          LOCAL_VERSION=$(awk '/"version":/{gsub(/("|",)/,"",$2);print $2}' packages/assemblyscript-plugin-sdk/package.json)
          if ! npm view @signalk/assemblyscript-plugin-sdk@$LOCAL_VERSION version &>/dev/null; then
            cd packages/assemblyscript-plugin-sdk
            npm publish --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Set tag variable
        id: vars
        run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT

      - name: Publish signalk-server
        run: |
          if [[ "${{ steps.vars.outputs.tag }}" == *beta* ]];
            then
              npm publish --tag beta
            else
              npm publish
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  release:
    permissions:
      contents: write
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    needs: build_and_publish
    steps:
      - name: Build Changelog
        id: github_release
        uses: mikepenz/release-changelog-builder-action@v6
        with:
          ignorePreReleases: 'true'
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}

      - name: Create Release (archived, need to be updated to use the new action)
        uses: actions/create-release@v1
        with:
          tag_name: ${{ github.ref }}
          release_name: ${{ github.ref }}
          prerelease: ${{ contains(github.ref, 'beta') }}
          body: ${{steps.github_release.outputs.changelog}}
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}

  docker_images:
    needs: build_and_publish
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        vm: [ubuntu-latest, ubuntu-24.04-arm]
        include:
          - vm: ubuntu-latest
            arch: amd
            platform: linux/amd64
          - vm: ubuntu-24.04-arm
            arch: arm
            node: 24.x
            platform: linux/arm64
          - os: 24.04
            node: 24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            node_safe: 24

    runs-on: ${{ matrix.vm }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Login to ghcr.io
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_PAT }}
      - name: Set TAG for build-args
        id: vars
        run: echo "tag=${GITHUB_REF#refs/*/v}" >> $GITHUB_OUTPUT
      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          file: ./docker/Dockerfile_rel
          platforms: ${{ matrix.platform }}
          push: true
          tags: ghcr.io/signalk/signalk-server:${{ matrix.arch }}-${{ matrix.os }}-${{ matrix.node_safe }}-${{ github.run_id }}
          build-args: |
            TAG=${{ steps.vars.outputs.tag }}
            BASE_IMAGE=${{ matrix.os }}-${{ matrix.node_safe }}

  create-and-push-manifest:
    needs: docker_images
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            tag: latest
            node_safe: 24.x
          - os: alpine
            node: 24.x
            tag: latest
            suffix: -alpine
            node_safe: 24

    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Docker meta
        id: docker_meta
        uses: docker/metadata-action@v6
        with:
          images: |
            ghcr.io/signalk/signalk-server
          tags: |
            type=semver,pattern={{raw}}
            type=semver,pattern=v{{major}},enable=${{ !contains(github.ref, 'beta') }}
            type=semver,pattern=v{{major}}.{{minor}},enable=${{ !contains(github.ref, 'beta') }}
            type=raw,value=${{ matrix.tag }},enable=${{ !contains(github.ref, 'beta') }}
          flavor: |
            suffix=${{ matrix.suffix }}
            latest=false
      - name: Login to ghcr.io
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_PAT }}
      - name: Create and push multi-arch manifest to GHCR
        uses: int128/docker-manifest-create-action@v2
        with:
          tags: |
            ${{ steps.docker_meta.outputs.tags }}
          sources: |
            ghcr.io/signalk/signalk-server:amd-${{ matrix.os }}-${{ matrix.node_safe }}-${{ github.run_id }}
            ghcr.io/signalk/signalk-server:arm-${{ matrix.os }}-${{ matrix.node_safe }}-${{ github.run_id }}
      - name: Save tags to file
        run: |
          mkdir -p /tmp/tags
          echo "${{ steps.docker_meta.outputs.tags }}" > /tmp/tags/${{ matrix.node_safe }}.txt
      - name: Upload tag artifact
        uses: actions/upload-artifact@v7
        with:
          name: release-tag-${{ matrix.node_safe }}
          path: /tmp/tags/${{ matrix.node_safe }}.txt
          retention-days: 1

  copy-to-dockerhub:
    needs: create-and-push-manifest
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            node_safe: 24.x
          - os: alpine
            node: 24.x
            node_safe: 24
    steps:
      - name: Download tag artifact
        uses: actions/download-artifact@v8
        with:
          name: release-tag-${{ matrix.node_safe }}
          path: /tmp/tags

      - name: Install skopeo
        run: |
          sudo apt-get update
          sudo apt-get install -y skopeo

      - name: Copy images from GHCR to Docker Hub
        shell: bash
        env:
          GHCR_USERNAME: ${{ github.actor }}
          GHCR_TOKEN: ${{ secrets.GHCR_PAT }}
          DOCKER_HUB_USERNAME: signalkci
          DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
        run: |
          set -euo pipefail
          TAGS_FILE="/tmp/tags/${{ matrix.node_safe }}.txt"
          while IFS= read -r FULL_TAG || [ -n "${FULL_TAG:-}" ]; do
            [ -z "${FULL_TAG:-}" ] && continue
            TAG="${FULL_TAG##*:}"
            echo "Copying: ${FULL_TAG} -> signalk/signalk-server:${TAG}"
            skopeo copy --all \
              --src-creds "${GHCR_USERNAME}:${GHCR_TOKEN}" \
              --dest-creds "${DOCKER_HUB_USERNAME}:${DOCKER_HUB_ACCESS_TOKEN}" \
              "docker://${FULL_TAG}" \
              "docker://docker.io/signalk/signalk-server:${TAG}"
          done < "$TAGS_FILE"

  housekeeping:
    needs: [copy-to-dockerhub]
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - name: Wait for GHCR indexing
        run: sleep 60
      - name: Remove Temporary & Untagged Docker Images from GHCR
        continue-on-error: true
        uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          packages: signalk-server
          delete-untagged: true
          delete-tags: |
            *-${{ github.run_id }}
          token: ${{ secrets.GHCR_PAT }}

  deploy_fly:
    runs-on: ubuntu-latest
    needs: copy-to-dockerhub
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Setup flyctl
        uses: superfly/flyctl-actions/setup-flyctl@master
      - name: Set TAG for build-arg
        id: vars
        run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
      - name: Deploy demo.signalk.org at fly.io
        working-directory: ./fly_io/demo_signalk_org
        run: flyctl deploy --remote-only --build-arg SK_VERSION=${{ steps.vars.outputs.tag }}
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}


================================================
FILE: .github/workflows/require_pr_label.yml
================================================
name: Pull Request Labels
on:
  pull_request:
    types: [opened, labeled, unlabeled, synchronize]
jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - uses: mheap/github-action-required-labels@v5
        with:
          mode: exactly
          count: 1
          labels: 'fix, feature, doc, chore, test, ignore, other, dependencies, refactor'


================================================
FILE: .github/workflows/security-scan.yml
================================================
name: Security Scan for Docker Images

on:
  schedule:
    - cron: '0 0 * * 1'
  workflow_dispatch:

permissions:
  contents: read
  security-events: write

jobs:
  security-scan:
    if: github.repository == 'SignalK/signalk-server'
    name: Security Scan
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        os: [24.04, alpine]
        node: [24.x]
        include:
          - os: 24.04
            node: 24.x
            image-tag: 'latest'
          - os: alpine
            node: 24.x
            image-tag: 'latest-alpine'
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
      - name: Run Trivy vulnerability scanner for Docker Image
        uses: aquasecurity/trivy-action@0.35.0
        with:
          image-ref: 'ghcr.io/signalk/signalk-server:${{ matrix.image-tag }}'
          format: 'sarif'
          output: 'trivy-results-docker-node${{ matrix.node }}-${{ matrix.image-tag }}.sarif'
      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: 'trivy-results-docker-node${{ matrix.node }}-${{ matrix.image-tag }}.sarif'
          category: 'trivy-docker-node${{ matrix.node }}-${{ matrix.image-tag }}'


================================================
FILE: .github/workflows/test.yml
================================================
name: CI test

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  pull_request:
  push:
    branches: [master]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [22.x, 24.x]

    steps:
      - uses: actions/checkout@v6
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
      - uses: SocketDev/action@v1
        with:
          mode: firewall-free
      - run: sfw npm install
      - run: npm run build:all

      - name: resources-provider-plugin
        working-directory: ./packages/resources-provider-plugin
        run: |
          npm run test

      - run: npm test
        env:
          CI: true


================================================
FILE: .gitignore
================================================
node_modules
!test/plugin-test-config/node_modules/

*.tsbuildinfo

lib/
dist/

.DS_Store
.vscode/
*.db
logs/*
bower_components
settings/ssl-key.pem
settings/ssl-cert.pem
/*.iml

*.tgz
work/
data/
plugin-config-data/
unitpreferences/custom-units-definitions.json
unitpreferences/custom/
unitpreferences/presets/custom/

test/plugin-test-config/.npmrc
test/plugin-test-config/plugin-config-data/
test/plugin-test-config/ssl-cert.pem
test/plugin-test-config/ssl-key.pem
test/plugin-test-config/baseDeltas.json
test/plugin-test-config/unitpreferences/

test/server-test-config/applicationData/
test/server-test-config/unitpreferences/
test/server-test-config/ssl-cert.pem
test/server-test-config/ssl-key.pem
test/server-test-config/plugin-config-data/

docs/built

# WASM build artifacts
*.wasm  

# .net builds
jco-output/
obj/
bin/
Debug/
Release/
*.ps1
nul

# wasmtime build artifacts
*.wasm

# debug artefacts
*.sln
test/server-test-config/debug

================================================
FILE: .mocharc.js
================================================
module.exports = {
  require: ['ts-node/register'],
  extensions: ['ts', 'tsx', 'js'],
  timeout: 20000,
  exit: true
}


================================================
FILE: .npmignore
================================================
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

src
public_src
webpack.config.js
scss
compilation-stats.json
.babelrc
public/*.hot-update.js
public/*.hot-update.json
**/stats.json
**/*.js.map

plugin-config-data/

samples/*
!samples/plaka.log
!samples/aava-n2k.data

work/*

npm-debug.log
.vscode

letsencrypt

docker

test
dist/**/*.test.js*

packages

# Build artifacts from build-docker.yml workflow
*.tgz

publishing.md

bin/linkpackages

fly_io
.github

/docs/*
!/docs/dist


# .net builds
jco-output/
obj/
bin/
Debug/
Release/
*.ps1
nul

# wasmtime build artifacts
*.wasm

# debug artefacts
*.sln



================================================
FILE: .npmrc
================================================
package-lock=false
legacy-peer-deps=true


================================================
FILE: .nvmrc
================================================
24


================================================
FILE: .prettierignore
================================================
*.guard.ts
public

**/*.mbtiles
**/*.pmtiles
**/.__mf__temp
# AssemblyScript build outputs
packages/assemblyscript-plugin-sdk/build


================================================
FILE: .prettierrc.json
================================================
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "overrides": [
    {
      "files": ["**/assembly/**/*.ts"],
      "options": {
        "plugins": ["assemblyscript-prettier"]
      }
    }
  ]
}


================================================
FILE: .python-version
================================================
3.11


================================================
FILE: AGENTS.md
================================================
# Signal K Server

Signal K Server is the reference implementation of a [Signal K](https://signalk.org/) server. Signal K is a modern, open data format and API for marine data. The server aggregates data from various sources (NMEA 0183, NMEA 2000, I2C sensors, etc.), provides a real-time WebSocket API and REST API, and supports a plugin architecture for extensibility.

Key components:

- **Core server**: Express-based HTTP/WebSocket server (TypeScript)
- **Plugin system**: NPM-based plugins with configuration schemas
- **Admin UI**: React-based web interface (packages/server-admin-ui)
- **Provider patterns**: ResourceProvider, WeatherProvider, AutopilotProvider, HistoryProvider

## Code Quality Principles

### Scope and Complexity

Follow YAGNI, SOLID, DRY, and KISS principles. Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.

Do not add features, refactor code, or make "improvements" beyond what was asked. A bug fix does not need surrounding code cleaned up. A simple feature does not need extra configurability.

Do not add error handling, fallbacks, or validation for scenarios that cannot happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs).

### General Standards

- Write self-documenting code; comments explain "why", not "what" - no echo comments restating what the code already says
- Keep functions small and focused on a single responsibility
- Prefer composition over inheritance
- Handle errors explicitly at system boundaries
- No magic numbers; use named constants
- Documentation describes current state, not development history - avoid changelog-style language that will become stale

### Type Safety

- **All new code must be written in TypeScript**, not JavaScript
- When converting JavaScript to TypeScript use pre-existing types when possible instead of creating new local types
- Use strict type checking; avoid `any` or equivalent escape hatches
- Validate external inputs at system boundaries
- Prefer immutable data structures where practical

### Testing

- All new code requires tests
- Test behavior, not implementation details
- Unit tests for business logic; integration tests for boundaries
- Aim for meaningful coverage, not arbitrary percentages

## Performance

Signal K Server runs on Raspberry Pi 3-5 hardware, often on battery power. CPU cycles cost watts. Treat the delta ingestion and fanout path as allocation-sensitive.

### Hot paths

Assume 100+ deltas/sec, 20+ WebSocket clients:

- `src/streambundle.ts` `pushDelta` / `push` — per value
- `src/subscriptionmanager.ts` subscriber callbacks — per delta per client
- `src/interfaces/ws.ts` `onChange`, `data` handler — per client, per message
- `src/BackpressureManager.ts` `send` — per delta per client
- `src/deltacache.ts` `onValue` — per delta
- `src/interfaces/rest.js` tree traversal — per HTTP request

### Rules

- **Guard `debug()` arguments.** `debug('x=' + JSON.stringify(obj))` evaluates eagerly even when disabled. Wrap with `debug.enabled &&` (see `src/interfaces/tcp.ts`).
- **Build objects in their final shape.** On hot paths, write all properties in a single object literal with consistent key order (V8 hidden class / shape optimization). Do not build up objects incrementally via spread or `Object.assign`.
- **Minimize allocations on the per-delta path.** Hoist constants, `Set`s, and closures to module scope. Do not `reduce` into a new array when nothing was removed. Prefer `for...of` over `.forEach`.
- **Use `structuredClone`**, not `JSON.parse(JSON.stringify(...))`, for deep cloning.
- **Prefer `Set` over `Array.includes`** for repeated membership checks.
- **Avoid lodash on hot paths.** `_.get`/`_.set` re-parse path strings; use `obj?.a?.b`. `_.isUndefined(x)` is just `x === undefined`.

## Git Commit Conventions

Use conventional format: `<type>(<scope>): <subject>` where type = feat|fix|docs|style|refactor|test|chore|perf. Subject: 50 chars max, imperative mood ("add" not "added"), no period. For small changes: one-line commit only. For complex changes: add body explaining what/why (72-char lines) and reference issues.

Keep commits small and atomic - one logical change per commit. Split unrelated changes into separate commits. The commit history tells a story; each commit should be a meaningful, self-contained step.

**MANDATORY:** Always rebase and clean up commit history before creating a PR or pushing changes. Amend fixes and corrections to the relevant existing commit instead of creating chains of "fix typo" or "oops" commits. The final history should contain only intentional, complete commits - no work-in-progress artifacts.

## Pull Request Guidelines

Before opening a PR:

- Branch from latest `master`
- Run `npm run format` and `npm test` - all checks must pass
- Rebase and clean up commit history (squash intermediate commits)
- Self-review your changes
- **NEVER change version numbers** - maintainers will update versions when publishing releases

PR titles are used to generate release notes. Make them **descriptive, informative, and easy to understand**. Ask yourself: "If someone only read the title, would they understand what this PR does?"

PR descriptions must be **succinct and straight to the point**. Explain the motivation (why) and summarize the solution approach (how), but not the mechanics (what) - the diff shows what changed. Do not pad descriptions with unnecessary detail, verbose explanations, or self-congratulatory comments. If there are breaking changes, mention them explicitly. If a PR description includes a test plan with checkboxes, **all items must be checked** before the PR is ready for review - remove or complete any unchecked items.

When referencing issues, use `closes`, `fixes`, or `resolves` followed by the issue number (e.g., "closes #18", "fixes #21 and resolves #23").

**MANDATORY:** One logical change per PR. Refactoring and behavior changes belong in separate PRs. If changes would result in multiple changelog entries, they should be separate PRs. Even if you have made multiple changes together locally, split them into separate PRs.

**AI tools must proactively enforce PR scope.** If a user requests changes unrelated to the current PR topic, do not silently include them. Instead, suggest creating a separate PR for the unrelated work. Similarly, when rebasing or cleaning up commit history, if you detect commits that address different topics, suggest splitting them into separate PRs before proceeding.

When updating a branch with upstream changes, **always use rebase, never merge commits**:

```shell
git fetch origin
git rebase origin/master
```


================================================
FILE: CHANGELOG.md
================================================
## Please see [Releases](https://github.com/SignalK/signalk-server-node/releases) for the release notes.


================================================
FILE: CLAUDE.md
================================================
@AGENTS.md


================================================
FILE: CONTRIBUTING.md
================================================
---
title: Contributing
---

# Contributing

Signal K server is an Open Source project and contributions are welcome.

Contributions are made by creating Pull Requests in the [GitHub repository](https://github.com/SignalK/signalk-server).

_**Working on your first Pull Request?**_

You can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)

---

### Additional Guidelines

The [AGENTS.md](AGENTS.md) file contains detailed coding guidelines primarily intended for AI coding assistants. The content is equally relevant for human contributors - it covers code quality principles, commit conventions, and PR guidelines that help maintain consistency across the project.

Don't worry if some of the instructions seem overly specific or prescriptive - they're written to give AI tools explicit guardrails. As a human, use your judgment; the spirit of the guidelines matters more than following every detail to the letter.

---

### Running the development server

1. Clone the repository:

   ```shell
   git clone https://github.com/SignalK/signalk-server
   cd signalk-server
   ```

1. Install dependencies:

   ```shell
   npm install
   ```

1. Build the server and related packages:

   ```shell
   npm run build:all
   ```

1. Start the server:
   ```shell
   npm start
   ```

The server should now be available at [http://localhost:3000](http://localhost:3000).

As you work on your changes, you may need to re-build changes. To continuously watch for changes, open a new terminal and run `npm run watch` in either the project root, or from the relevant directory in `packages/*`.

You may also need to restart the server to see some changes reflected.

### Using sample data

Start the server with sample data by running:

- NMEA0183 sample data: `bin/nmea-from-file`
- NMEA2000 sample data: `bin/n2k-from-file`

This will start the server with a sample configuration file and the server will start playing back data from a sample file under `samples/`. The data is available immediately via the REST interface at https://localhost:3000/signalk/v1/api/ and via WebSocket, for example with

```
npm install -g wscat2
wscat 'ws://localhost:3000/signalk/v1/stream?subscribe=all'
```

### Submitting a Pull Request (PR)

Before you submit your Pull Request (PR) consider the following guidelines:

1. [Fork](https://help.github.com/articles/fork-a-repo/) the repository.
1. Make your changes in a new git branch:
   - `git checkout -b my-fix-branch master`
   - Do not change the server or package version numbers. They will be changed by the maintainers after the PR is merged, when a new version is published
   - Create separate PRs for separate things - don't cram unrelated things to one PR, even if you have done them together. If you put multiple changes in one PR and one gets stalled or rejected we could still possibly merge the other one. If changes in one depend on the other one state that in PR description. You can think in terms of release notes: if the changes would be two entries in the changelog they should be separate PRs.
1. Commit your changes using a descriptive commit message that follows the
   [conventions outlined here](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits). Whilst we are not 100% strict about this, it really helps when reviewing the PR and in making the commit history readable. The TL;DR of it is below.
   - The subject line should be in the format `<type>: <subject>`, where `<type>` should be one of:
     - feat (feature)
     - fix (bug fix)
     - docs (documentation)
     - style (formatting, missing semi colons, ...)
     - refactor
     - test (when adding missing tests)
     - chore (maintain)
   - `<subject>` should use imperative, present tense: "change" not "changed" or "changes"
   - Examples of good Subject Lines:
     - `doc: clarify meta.units behaviour`
     - `chore: update keyswithmetadata.json`
     - `style: prettier`
     - `fix: allow nextPoint to be an intermediate leaf`
     - `feature: push design object fields under value/values`
   - Message body should also be **in imperative, present tense** and **include motivation for the change** and **differences to previous behaviour**.
   - Footer should reference any issues. If the PR should close issue(s) (assuming it is committed), **use closes,fixes or resolves** and the issue number. eg. "closes #18", "fixes #21 and resolves #23".
   - Subject, Body and Footer are separated by a blank line.

1. Format and lint your code
   - run `npm run format` to format and [lint](<https://en.wikipedia.org/wiki/Lint_(software)>) your code.

1. Push your branch to GitHub:
   - `git push origin my-fix-branch`

1. In GitHub, create a pull request.
   - Use the same guidelines as commit messages to write the PR title and description.
   - The server's release notes are automatically generated from PR titles, so think about how you can make them **descriptive, informative and easy to understand**. Ask yourself: "If I only knew the title would I understand what the PR does?".
   - The description should tell how the change affects the server's behavior and motivation for doing the change.
   - If you change the Admin UI include screenshots in the description to help others get a quick idea what changes and how it will look. Before & after pictures are great for this.
   - If you are using AI **PLEASE TAKE THE TIME to make the PR description succinct and straight to the point**. AIs are really good at creating text, filling in lots of details and adding smug comments how great the PR is. HELP the maintainers so that we don't need to wade through AI fluff. We will ask for more details if too little are included.
   - Don't include too much detail, like the exact changed lines or a version you tested the change with unless there is specific reason to do so. If the change is not directly related to a version adding a version is misleading. Git shows what's changed and extra content in PR description is just double work for maintainers to read, unless there is something that rquires attention.

1. Wait for labeling and review
   - PRs are automatically reviewed by [CodeRabbit](https://coderabbit.ai/). Address any comments it raises. Once you are done addressing CodeRabbit's feedback, add a comment **"Ready for human review"** to signal that the PR is ready for maintainer review.
   - The maintainers will apply a label to the PR. The label is used to group PRs, mainly to distinguish fixes and new features.
   - If we require changes to your PR we expect you to:
   - Implement the agreed changes.
   - Rebase your branch and force push to your GitHub repository (this will update your Pull Request):

     ```shell
     git rebase master -i
     git push -f
     ```


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

================================================
FILE: Procfile
================================================
web: node bin/signalk-server -s ./settings/n2k-from-file-settings.json


================================================
FILE: README.md
================================================
# Signal K Server

![Signal K logo](https://user-images.githubusercontent.com/5200296/226164888-d33b2349-e608-4bed-965f-ebe4339b4376.png)

[![npm version](https://badge.fury.io/js/signalk-server.svg)](https://badge.fury.io/js/signalk-server)
[![npm license](https://img.shields.io/npm/l/signalk-server.svg)](https://www.npmjs.com/package/signalk-server)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)

[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/signalk)](https://opencollective.com/signalk)

## Contents

- [Signal K Server](#signal-k-server)
  - [Contents](#contents)
  - [Introduction](#introduction)
    - [Boaters and Boat Owners](#boaters-and-boat-owners)
    - [Marine Vendors](#marine-vendors)
    - [Software Developers \& Boat Electronics Hobbyists](#software-developers--boat-electronics-hobbyists)
  - [Signal K Platform](#signal-k-platform)
  - [Documentation, Community \& Support](#documentation-community--support)
  - [How to get Signal K Server?](#how-to-get-signal-k-server)
  - [Configuration and use](#configuration-and-use)
    - [Opening the Signal K Server Admin UI](#opening-the-signal-k-server-admin-ui)
    - [Creating an admin account](#creating-an-admin-account)
    - [Setting up data connections](#setting-up-data-connections)
    - [Installing Plugins and Webapps](#installing-plugins-and-webapps)
    - [Restart after Configuration Changes and Plugin/Webapp Installation](#restart-after-configuration-changes-and-pluginwebapp-installation)
    - [Configuring Plugins](#configuring-plugins)
    - [Vessel Base Data and Server Settings](#vessel-base-data-and-server-settings)
    - [Server Log](#server-log)
  - [Supported PGNs, sentences and more](#supported-pgns-sentences-and-more)
  - [Development](#development)
  - [Sponsoring Signal K](#sponsoring-signal-k)
  - [License](#license)

## Introduction

Signal K Server is a server application that runs on a central hub in a boat. If you use or develop marine electronics, Signal K Server has something to offer for you.

### Boaters and Boat Owners

For boaters, Signal K Server runs in the background and makes functionality and data available to other apps and devices.
One of its most used features is to be a wireless AIS and navigation server for popular apps like Navionics, iSailor, iNavX, Aqua Map and WilhelmSK on your phones and tablets.

Signal K Server can also take care of the anchor watch, be a weather station or an automatic logbook for you.
A different example, it can turn your boat into a MarineTraffic station which may give free access to [the MarineTraffic professional plans](https://help.marinetraffic.com/hc/en-us/articles/360017183497-As-a-station-owner-am-I-entitled-to-a-free-Subscription-).
These are all just examples: there is far more to Signal K Server.

If you are a boat owner, you can easily run Signal K Server on a Victron Cerbo GX, RaspberryPi or similar hardware. To take full advantage, you will probably want to connect it to your boat network via NMEA 0183 or NMEA 2000 but it is not a requirement.

### Marine Vendors

For Marine vendors who build marine hardware and software, for example those developing navigation, monitoring and tracking systems, Signal K Server is an opportunity to accelerate development and decrease time to market, by taking advantage of a proven, modern and extensible software platform that is open source and available with a permissive Apache 2.0 license. Signal K Server is implemented in Node.js and is easy to integrate into modern systems that run Linux derivatives.

Signal K Server is already running inside products developed by Victron Energy, Airmar Technology and others.

### Software Developers & Boat Electronics Hobbyists

There are many boaters who happen to be highly skilled software developers and engineers, who want to build software for themselves and share with others. If you are one of them, Signal K offers you a free, modern and open platform developed by boaters for other boaters like you. Signal K Server features an extensible [plugin framework](./docs/develop/plugins/README.md), [web applications](./docs/develop/webapps.md) as well as a rich set of [REST](https://signalk.org/specification/1.7.0/doc/rest_api.html) and [Streaming APIs](https://signalk.org/specification/1.7.0/doc/streaming_api.html).

Signal K Server takes care of all the complicated parts of protocol decode, and conversions to and from NMEA2000, NMEA0183 and many more protocols. It can also act as data hub for additional sensors, see the [Signal K SensESP project](https://github.com/SignalK/SensESP) for [ESP32](https://en.wikipedia.org/wiki/ESP32).

Signal K Server makes the data available in JSON format according to the [Signal K standard specification](https://signalk.org/specification/latest/). This allows developers to bypass all the hurdles typically encountered when wanting to implement something for a boat. [Getting started with a plugin](./docs/develop/plugins/README.md#getting-started-with-plugin-development) is surprisingly easy.

## Signal K Platform

Signal K is more than just the Signal K Server, it is a comprehensive platform that encompasses three major components:

1. **The Signal K Data Standard**: an open marine data standard. It is a modern data format for marine use, suitable for WiFi, cellphones, tablets and the internet. It is built on standard web technologies including JSON, WebSockets and HTTP. More information on [https://signalk.org](https://signalk.org/index.html).
2. **Signal K Server**: Software in this GitHub repository and described in this document. Signal K server is a full stack application developed in Node.js. Its back-end multiplexes data from and to NMEA0183, NMEA 2000, Signal K and other marine protocols, as well as WiFi, LAN and Internet, and provides APIs and websockets for access and control. Its front-end provides an extensible web-based application allowing easy configuration and management of server functions and capabilities.
3. **Signal K Plugins and Webapps**: Built using the extensibility of Signal K Server with a plugin framework, allows developers to develop applications that easily integrate with Signal K server, extend its capabilities and publish them through npm. All published plugins become available in all existing Signal K server installations, which provides an easy distribution mechanism.

## Documentation, Community & Support

[Documentation for Signal K Server](https://demo.signalk.org/documentation).

See [Github Discussions](https://github.com/SignalK/signalk/discussions/) and [Discord (chat)](https://discord.gg/uuZrwz4dCS).

There is a [Signal K Server FAQ Frequently Asked Questions](https://github.com/SignalK/signalk-server/wiki/FAQ:-Frequently-Asked-Questions) on the Wiki, including [How do I integrate with NMEA2000 (CAN bus)](https://github.com/SignalK/signalk-server/wiki/FAQ:-Frequently-Asked-Questions#how-do-i-integrate-with-nmea2000-can-bus).

## How to get Signal K Server?

For the typical boater, not being a software developer nor electrical engineer, the best option is to get a (commercially available) product that already has Signal K Server inside. These are the currently available devices:

- [Cerbo GX](https://www.victronenergy.com/panel-systems-remote-monitoring/cerbo-gx) and other GX Devices by Victron Energy ([see Venus OS Large manual](https://www.victronenergy.com/live/venus-os:large))
- [SmartBoat module](https://www.airmar.com/Catalog/SmartBoat-SmartFlex) by Airmar

For a more technical DIY oriented boater, a RaspberryPi based setup offers a very cost-attractive alternative.
Read [this FAQ entry](https://github.com/SignalK/signalk-server/wiki/FAQ:-Frequently-Asked-Questions#how-do-i-integrate-with-nmea2000-can-bus) to learn how to connect a RaspberryPi to an NMEA2000 network.

These prebuilt images for RaspberryPis take away most of the complexity involved from the software side:

- [BBN Marine OS](https://github.com/bareboat-necessities/lysmarine_gen#what-is-lysmarine-bbn-edition)
- [OpenPlotter](https://openmarine.net/openplotter) by OpenMarine
- [Venus OS for RaspberryPis](https://github.com/victronenergy/venus/wiki/raspberrypi-install-venus-image) by Victron Energy
- [AvNav Headless/Touch](https://github.com/free-x/AvNav-Image)

You can run Signal K Server in Docker:

- [Docker quickstart instructions](https://github.com/SignalK/signalk-server/blob/master/docker/README.md#quickstart)

Or in a Kubernetes cluster:

- [Kubernetes quickstart instructions](https://github.com/SignalK/signalk-server/blob/master/kubernetes/README.md#quickstart)

And an installer for Windows:

- [https://github.com/SignalK/signalk-server-windows](https://github.com/SignalK/signalk-server-windows)

Another level up, this document explains how to install Signal K Server, as well as its dependencies, on a RaspberryPi that is already running Raspberry Pi OS:

- [Installation on a RaspberryPi](./docs/installation/raspberry_pi_installation.md)

Last, here is how to install the Signal K Server application from NPM:

Prerequisites:

- Node.js version 24 with latest npm installed

  $ sudo npm install -g signalk-server

Now you can start the server with sample data:

- NMEA0183 sample data: `signalk-server --sample-nmea0183-data`
- NMEA2000 sample data: `signalk-server --sample-n2k-data`

To generate your own vessel settings file and configure the server to start automatically, run:

    $ sudo signalk-server-setup

## Configuration and use

### Opening the Signal K Server Admin UI

For all described options of running Signal K Server, ie. on an Airmar Smartboat, a Victron Cerbo GX or a RaspberryPi, the way to configure it is via the Admin UI.
Open the Admin UI by navigating to http://[ipaddress]:3000/. Here is what it will look like when opened up on a Victron Cerbo GX:

![image](https://user-images.githubusercontent.com/5200296/226478726-568d8ea3-5f46-4e7b-b964-4fdefb386c32.png)

The top of the screen shows some actual stats. Below that is a pane showing all configured Connections & Plugins. These are the plugins shown in above screenshot:

- `sk-to-nmea0183` is the plugin that makes navigation data available on WiFi and/or LAN (TCP); typically used by apps on phones and tablets.
- `signalk-n2kais-nmea0183` is another plugin, does the same, but then for AIS data
- `venus` is a plugin that connects to the data bus inside the Victron GX device
- `n2k-on-ve.can-socket` is not a plugin but a data connection. This one defines the Signal K Server connection to the NMEA2000 CAN-bus port.

### Creating an admin account

The first thing to do is create an admin account. This is done in the Settings -> Users page:

![image](https://user-images.githubusercontent.com/5200296/226754646-3bc60ddb-245a-4bd2-ab2f-b5539bdefa77.png)

Besides recommended from a security point of view, setting an admin account also enables the Restart button.

After creating the account, the server needs to be restarted.
How to do that depends on how you are using Signal K Server: self installed from NPM, embedded on a commercial device or otherwise.
Power cycling the device that Signal K Server is running on will always work.

### Setting up data connections

This screenshot shows how to setup an NMEA0183 connection:

![image](https://user-images.githubusercontent.com/5200296/226479444-853570cb-83ea-4246-afbe-06cafd48d790.png)

### Installing Plugins and Webapps

The Appstore menu is where to add, update and remove Plugins and Webapps:

![image](https://user-images.githubusercontent.com/5200296/226479620-303a2e6e-a4f7-4ecb-b1f1-a668fb147d23.png)

The entries with the blue icons are Webapps. The entries with the green icons are Plugins. An internet connection is required for Signal K Server to fetch information about availble Plugins and webapps.

Typically, plugins make for functionality such as protocol conversion. And Webapps provide a user interface, up to a fully featured Chartplotter that runs in a web browser:

![image](https://user-images.githubusercontent.com/5200296/226479871-6f3769af-4fa4-43d6-871f-4a54bec372fa.png)

To install Plugins and Webapps, click the "Available" menu on the left. It will show a categorised list of all available Plugins:

![image](https://user-images.githubusercontent.com/5200296/226480596-f65f5429-57d5-4d31-bb13-615d5664e2c4.png)

It is also possible to search for and browse Plugins and Webapps in the NPM registry:

- [Plugins](https://www.npmjs.com/search?q=keywords%3Asignalk-node-server-plugin)
- [Webapps](https://www.npmjs.com/search?q=keywords:signalk-webapp)

### Restart after Configuration Changes and Plugin/Webapp Installation

Most configuration changes and installing add-ons from the App store require a server restart to take effect. See Restart button at the top right or restart the server manually (details depend on your setup). If the restart button is not showing, that is usually because security is not activate and there is no Admin user.

### Configuring Plugins

After the restart, the new Plugin needs to be enabled and configured. See the Server -> Plugin Config menu:

![image](https://user-images.githubusercontent.com/5200296/226481818-18c5cbe1-9118-4555-ab8b-1622c3e9404b.png)

### Vessel Base Data and Server Settings

![image](https://user-images.githubusercontent.com/5200296/226482046-dfb759dc-abbb-4987-a810-a24b77d0927e.png)

![image](https://user-images.githubusercontent.com/5200296/226482099-b9dd46ff-72a6-44e4-b384-1d15a4621e63.png)

You can change the admin application's top left logo by placing a SVG file named `logo.svg` in the settings directory (default: $HOME/.signalk/). You can also provide a minimized square version of the logo in a file named `logo-minimized.svg` that will be shown when the sidebar is minimized.

### Server Log

If the Admin UI is available, go to Server -> Server Log to see the server's log. Different errors are logged there, so in case of trouble make sure to check not only the Admin UI but also the server log.

To activate more details debug logging enter the the names of the components you want to debug. Some of the debug keys are listed with toggles to activate them.

With the Remember debug setting enabled, the configured debug keys parameter is stored in a settings file, ie. survives a server restart.

![image](https://user-images.githubusercontent.com/5200296/227020518-ac8b4355-5902-45a5-9d6c-0e9d1dc9e630.png)

To enable debugging without going through the Admin UI, see the file `~/.signalk/debug` and add the required debug keys there. For example: `@signalk/aisreporter,signalk-server:udp-provider`.

## Supported PGNs, sentences and more

- NMEA2000 PGNs: Reading NMEA2000 data is done by [n2k-signalk](https://github.com/SignalK/n2k-signalk) via [canboatjs](https://github.com/canboat/canboatjs). [Canboat PGN database](https://canboat.github.io/canboat/canboat.html)
- NMEA0183 sentences: [nmea0183-signalk](https://github.com/SignalK/signalk-parser-nmea0183)
- TODO ADD OTHER SUPPORTED PROTOCOLS

## Development

The documents provide more details about developing Webapps or Plugins for Signal K Server, as well as working on the server itself:

- [Contributing to this repo](docs/develop/contributing.md)
- [WASM Plugins](docs/develop/plugins/wasm/README.md) (Rust, AssemblyScript, Go)
- [Webapps](docs/develop/webapps.md)
- [Working with the Course API](docs/develop/rest-api/course_api.md)
- [Working with the Resources API](docs/develop/rest-api/resources_api.md)
- [Resource Provider Plugins](docs/develop/plugins/resource_provider_plugins.md)
- [Security](docs/security.md)

## Sponsoring Signal K

See Signal K on [Open Collective](https://opencollective.com/signalk).

## License

Copyright [2015] [Fabian Tollenaar, Teppo Kurki and Signal K committers]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


================================================
FILE: docker/Dockerfile
================================================
ARG REGISTRY="cr.signalk.io"
ARG BASE_IMAGE="24.04-22.x"

FROM ${REGISTRY}/signalk/signalk-server-base:latest-${BASE_IMAGE} AS base

USER node
RUN mkdir -p /home/node/.signalk/ \
  && mkdir -p /home/node/signalk/

WORKDIR /home/node/signalk

RUN npm config rm proxy \
  && npm config rm https-proxy \
  && npm config set fetch-retries 5 \
  && npm config set fetch-retry-mintimeout 60000 \
  && npm config set fetch-retry-maxtimeout 120000 \
  && npm cache clean -f

FROM base AS tarballs_installed
WORKDIR /home/node/signalk
COPY --chown=node:node *.tgz .

# Install all tarballs locally (not globally) to avoid permission issues
# Install workspace packages first so they're available when installing signalk-server
RUN npm init -y \
  && workspace_tarballs=$(ls *.tgz 2>/dev/null | grep -v '^signalk-server-[0-9]' || true) \
  && if [ -n "$workspace_tarballs" ]; then npm install $workspace_tarballs; fi \
  && server_tarball=$(ls signalk-server-[0-9]*.tgz) \
  && npm install "$server_tarball" --install-strategy=nested \
  && if [ -d node_modules/@signalk ]; then \
       mkdir -p node_modules/signalk-server/node_modules/@signalk/ \
       && cp -rf node_modules/@signalk/* node_modules/signalk-server/node_modules/@signalk/ \
       && rm -rf node_modules/@signalk/; \
     fi \
  && rm *.tgz

FROM base

# Copy locally installed modules from tarballs_installed stage
COPY --from=tarballs_installed --chown=node:node /home/node/signalk /home/node/signalk

COPY --chown=node:node --chmod=755 docker/startup.sh startup.sh

EXPOSE 3000
ENV SKIP_ADMINUI_VERSION_CHECK=true
WORKDIR /home/node/.signalk
ENTRYPOINT ["/home/node/signalk/startup.sh"]


================================================
FILE: docker/Dockerfile_base_24.04
================================================
FROM ubuntu:24.04
ARG NODE
ENV DEBIAN_FRONTEND=noninteractive

RUN userdel -r ubuntu \
  && groupadd --gid 1000 node \
  && useradd --uid 1000 --gid node --shell /bin/bash --create-home node

COPY docker/avahi/avahi-dbus.conf docker/bluez/bluezuser.conf /etc/dbus-1/system.d/

RUN apt-get update \
  && apt-get -y install --no-install-recommends git \
  python3 python3-venv python3-pip build-essential avahi-daemon libnss-mdns \
  sysstat procps nano curl libcap2-bin sudo dbus bluez \
  && groupadd -r docker -g 991 \
  && groupadd -r i2c -g 990 \
  && groupadd -r spi -g 989 \
  && usermod -a -G dialout,i2c,spi,netdev,docker node \
  && chmod u+s /usr/bin/date \
  && mkdir -p /var/run/dbus/ /var/run/avahi-daemon/ \
  && chmod -R 777 /var/run/dbus/ /var/run/avahi-daemon/ \
  && chown -R avahi:avahi /var/run/avahi-daemon/

RUN curl -fsSL https://deb.nodesource.com/setup_$NODE | bash - \
  && apt-get -y install --no-install-recommends nodejs \
  && npm config rm proxy \
  && npm config rm https-proxy \
  && npm config set fetch-retries 5 \
  && npm config set fetch-retry-mintimeout 60000 \
  && npm config set fetch-retry-maxtimeout 120000 \
  && npm cache clean -f \
  && npm install npm@latest -g \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*


================================================
FILE: docker/Dockerfile_base_alpine
================================================
ARG NODE
FROM node:${NODE}-alpine

COPY docker/avahi/avahi-dbus.conf docker/bluez/bluezuser.conf /etc/dbus-1/system.d/

RUN deluser node 2>/dev/null || true && \
    delgroup node 2>/dev/null || true

RUN apk add --no-cache \
    sudo git bash \
    python3 py3-virtualenv py3-pip build-base \
    avahi avahi-compat-libdns_sd \
    procps-ng nano curl libcap-setcap dbus bluez \
 && addgroup -g 991 -S docker \
 && addgroup -g 990 -S i2c \
 && addgroup -g 989 -S spi \
 && mkdir -p /var/run/dbus /var/run/avahi-daemon \
 && chmod 777 /var/run/dbus /var/run/avahi-daemon \
 && chown -R avahi:avahi /var/run/avahi-daemon

RUN npm config rm proxy \
 && npm config rm https-proxy \
 && npm config set fetch-retries 5 \
 && npm config set fetch-retry-mintimeout 60000 \
 && npm config set fetch-retry-maxtimeout 120000 \
 && npm cache clean -f \
 && rm -rf /tmp/* /var/tmp/*

RUN addgroup -g 1000 node \
 && adduser -u 1000 -G node -s /bin/sh -D node \
 && addgroup node dialout \
 && addgroup node i2c \
 && addgroup node spi \
 && addgroup node netdev \
 && addgroup node docker \
 && echo 'node ALL=(ALL) NOPASSWD:/bin/date' >> /etc/sudoers

WORKDIR /home/node
USER node


================================================
FILE: docker/Dockerfile_rel
================================================
ARG REGISTRY="cr.signalk.io"
ARG BASE_IMAGE="24.04-22.x"

FROM ${REGISTRY}/signalk/signalk-server-base:latest-${BASE_IMAGE}

USER node
RUN mkdir -p /home/node/.signalk/ \
  && mkdir -p /home/node/signalk/

WORKDIR /home/node/signalk

RUN npm config rm proxy \
  && npm config rm https-proxy \
  && npm config set fetch-retries 5 \
  && npm config set fetch-retry-mintimeout 60000 \
  && npm config set fetch-retry-maxtimeout 120000 \
  && npm cache clean -f

ARG TAG

# Install locally (not globally) to avoid permission issues
# Copy @signalk packages and @mxtommy/kip to nested location where webapp discovery expects them
RUN npm install signalk-server@$TAG \
  && mkdir -p node_modules/signalk-server/node_modules/@signalk/ \
  && cp -rf node_modules/@signalk/* node_modules/signalk-server/node_modules/@signalk/ \
  && rm -rf node_modules/@signalk/ \
  && mkdir -p node_modules/signalk-server/node_modules/@mxtommy/ \
  && cp -rf node_modules/@mxtommy/kip node_modules/signalk-server/node_modules/@mxtommy/ \
  && rm -rf node_modules/@mxtommy/;

COPY --chown=node:node --chmod=755 docker/startup.sh startup.sh

EXPOSE 3000
WORKDIR /home/node/.signalk
ENTRYPOINT ["/home/node/signalk/startup.sh"]


================================================
FILE: docker/README.md
================================================
# General

Release process first publishes the server's modules to npm. Docker images are then built using the just published npm packages. Images (including older versions) are available at [Docker Hub](https://hub.docker.com/r/signalk/signalk-server) and starting from v2 at [GitHub Container registry](https://github.com/orgs/SignalK/packages/container/package/signalk-server). Going forward **use the full image name, including the registry cr.signalk.io**. That address will be updated to redirect to the recommended registry where the latest released version can be found.

Release images:

- cr.signalk.io/signalk/signalk-server:latest
- cr.signalk.io/signalk/signalk-server:`<release tag>`, e.g. `v2.16.0`

## Docker Images based on Ubuntu 24.04 LTS

### Node.js 22.x

**Image tag:** `v2.16.0` (example version)

**Supported architectures:**

- `linux/amd64`
- `linux/arm64`
- `linux/arm/v7`

### Node.js 24.x

**Image tag:** `v2.16.0-24.x` (example version with suffix -24.x)

**Supported architectures:**

- `linux/amd64`
- `linux/arm64`

**Not supported:**

- `linux/arm/v7`

### Important Note

Node.js version 24.x dropped support for the `linux/arm/v7` (ARMv7) architecture. This affects older hardware, particularly early Raspberry Pi models:

**Affected devices:**

- Raspberry Pi Model A and B (original)
- Raspberry Pi Zero (original)
- Raspberry Pi 2 Model B v1.1 (with BCM2836 processor)

**Recommendation:** If you're using any of the affected devices, use the Node.js 22.x images instead of 24.x to maintain compatibility.

# Quickstart

You can start a local server on port 3000 with demo data with

```
docker run --init -it --rm --name signalk-server --publish 3000:3000 --entrypoint /home/node/signalk/node_modules/.bin/signalk-server cr.signalk.io/signalk/signalk-server --sample-nmea0183-data
```

For real use you need to persist /home/node/.signalk where the server's configuration is stored, with for example

```
docker run -d --init  --name signalk-server -p 3000:3000 -v $(pwd):/home/node/.signalk cr.signalk.io/signalk/signalk-server
```

This will run the server as background process and current directory as the settings directory. You will be prompted to create admin credentials the first time you you access the configuration admin web UI.

## Docker Compose

See `docker/docker-compose.yml` for reference / example if you want to use docker-compose.

# Image details and used tags

Signal K Server docker images are based on Ubuntu 24.04 LTS. During build process, Node.js is installed including tools required to install or compile plugins. Signal K supports mDNS from docker, uses avahi for e.g. mDNS discovery. All required avahi tools and settings are available for user `node`, also from command line.

## Directory structure

- server files: `/home/node/signalk/` (local npm install)
- settings files and plugins: `/home/node/.signalk/`

You most probably want to mount `/home/node/.signalk` from the host or as a volume to persist your settings.

**Note:** Signal K Server is installed locally (not globally with `npm -g`) in `/home/node/signalk/node_modules/`. This avoids permission issues when installing plugins and provides better isolation.

## Container Runtime Detection

The server automatically detects which container runtime is being used (Docker, Podman, Kubernetes, etc.) and sets the `CONTAINER_RUNTIME` environment variable. Plugins can use this to adapt their behavior.

Supported runtimes: `docker`, `podman`, `kubernetes`, `containerd`, `crio`, `lxc`

## Release images

Release images `docker/Dockerfile_rel` are size optimized and there are only mandatory files in the images. During the release process updated npm packages in the server repo are built and published to npmjs. Release docker image is then built from the published npm packages like Signal K server is installed normally from npmjs.

## Development images

Development images `docker/Dockerfile`include all files from the Signal K server repository's master branch and these images are targeted mainly for development and testing. Development images are built off the files in the repo, including the submodules from `packages` directory.

Development images are tagged `<branch>` (mainly `master`) and `sha`:

```
docker run --init --name signalk-server -p 3000:3000 -v $(pwd):/home/node/.signalk cr.signalk.io/signalk/signalk-server:master
```

## Building from source

To build a docker image locally from source, first build and pack the server:

```sh
npm install
npm run build:all
npm pack --workspaces
npm pack
```

Then build the docker image:

```sh
$ docker build -t signalk-server:master -f docker/Dockerfile .
```

Now you can run the local image:

```sh
docker run --init --name signalk-server -p 3000:3000 -v $(pwd):/home/node/.signalk signalk-server:master
```


================================================
FILE: docker/avahi/avahi-dbus.conf
================================================
<!DOCTYPE busconfig PUBLIC
          "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
          "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>

  <!-- Only root or user avahi can own the Avahi service -->
  <policy user="avahi">
    <allow own="org.freedesktop.Avahi"/>
  </policy>
  <policy user="node">
    <allow own="org.freedesktop.Avahi"/>
  </policy>
  <policy user="root">
    <allow own="org.freedesktop.Avahi"/>
  </policy>

  <!-- Allow anyone to invoke methods on Avahi server, except SetHostName -->
  <policy context="default">
    <allow send_destination="org.freedesktop.Avahi"/>
    <allow receive_sender="org.freedesktop.Avahi"/>

    <deny send_destination="org.freedesktop.Avahi"
          send_interface="org.freedesktop.Avahi.Server" send_member="SetHostName"/>
  </policy>

  <!-- Allow everything, including access to SetHostName to users of the group "netdev" -->
  <policy group="netdev">
    <allow send_destination="org.freedesktop.Avahi"/>
    <allow receive_sender="org.freedesktop.Avahi"/>
  </policy>
  <policy user="root">
    <allow send_destination="org.freedesktop.Avahi"/>
    <allow receive_sender="org.freedesktop.Avahi"/>
  </policy>
</busconfig>


================================================
FILE: docker/bluez/bluezuser.conf
================================================
<!-- This configuration file specifies the required security policies
for a user to communicate with BlueZ. -->
 
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
    <policy user="node">
        <allow own="org.bluez"/>
        <allow send_destination="org.bluez"/>
        <allow send_interface="org.bluez.GattCharacteristic1"/>
        <allow send_interface="org.bluez.GattDescriptor1"/>
        <allow send_interface="org.bluez.LEAdvertisement1"/>
        <allow send_interface="org.freedesktop.DBus.ObjectManager"/>
        <allow send_interface="org.freedesktop.DBus.Properties"/>
    </policy>
</busconfig>

================================================
FILE: docker/docker-compose.yml
================================================
version: '2.2'
services:
  signalk-server:
    image: cr.signalk.io/signalk/signalk-server:latest
    container_name: signalk-server
    restart: unless-stopped
    #   ----------------------
    network_mode: host
    #    network_mode: bridge    # (1/3) If bridge-mode is used, then comment line abobe (host) and select/add needed ports settings
    #    ports:    # (2/3)
    #      - "3000:3000"    # (3/3)
    #   ----------------------
    #    environment:    # (1/5) SK ENV parameters
    #      - PORT=3000    # (2/5)
    #      - SSLPORT=3443    # (3/5)
    #      - NMEA0183PORT=10110    # (4/5)
    #      - TCPSTREAMPORT=8375    # (5/5)
    #   ----------------------
    volumes:
      - /dev:/dev
    #      - $PWD/signalk_conf:/home/node/.signalk    # uncomment and make signalk_conf -folder where .signalk is bind mounted
    #   ----------------------
    #      - type: bind    # (1/3) uncomment these 3 lines to control startup.sh outside container
    #        source: $PWD/startup.sh    # (2/3)
    #        target: /home/node/signalk/startup.sh    # (3/3)
    #   ----------------------
    entrypoint: sh /home/node/signalk/startup.sh
    privileged: true
    logging:
      options:
        max-size: 10m


================================================
FILE: docker/startup.sh
================================================
#!/usr/bin/env sh

# Detect container runtime (only if not already set by user)
if [ -z "$CONTAINER_RUNTIME" ]; then
    # File-based detection (most reliable)
    if [ -f /.dockerenv ]; then
        export CONTAINER_RUNTIME="docker"
    elif [ -f /run/.containerenv ]; then
        export CONTAINER_RUNTIME="podman"
    elif [ -n "$KUBERNETES_SERVICE_HOST" ]; then
        # Kubernetes sets this environment variable
        export CONTAINER_RUNTIME="kubernetes"
    else
        # Fallback: check cgroups and other markers
        if [ -f /proc/1/cgroup ]; then
            if grep -q '/docker' /proc/1/cgroup 2>/dev/null; then
                export CONTAINER_RUNTIME="docker"
            elif grep -q '/libpod' /proc/1/cgroup 2>/dev/null; then
                export CONTAINER_RUNTIME="podman"
            elif grep -q '/kubepods' /proc/1/cgroup 2>/dev/null; then
                export CONTAINER_RUNTIME="kubernetes"
            elif grep -q '/lxc' /proc/1/cgroup 2>/dev/null; then
                export CONTAINER_RUNTIME="lxc"
            elif grep -q '/containerd' /proc/1/cgroup 2>/dev/null; then
                export CONTAINER_RUNTIME="containerd"
            fi
        fi

        # Additional checks for CRI-O and other runtimes
        if [ -z "$CONTAINER_RUNTIME" ]; then
            if [ -d /var/run/crio ]; then
                export CONTAINER_RUNTIME="crio"
            elif [ -S /var/run/containerd/containerd.sock ]; then
                export CONTAINER_RUNTIME="containerd"
            fi
        fi
    fi
fi

# Set IS_IN_DOCKER for Admin UI (disables "Update Server" button in container)
export IS_IN_DOCKER=true

# Check if host D-Bus socket is mounted (rootless container scenario)
# If mounted, we use the host's D-Bus/Avahi instead of starting our own
if [ -S /run/dbus/system_bus_socket ] && dbus-send --system --dest=org.freedesktop.DBus --print-reply /org/freedesktop/DBus org.freedesktop.DBus.ListNames >/dev/null 2>&1; then
    echo "Using host D-Bus (socket mounted from host)"
else
    echo "Starting container D-Bus and Avahi services"
    service dbus restart
    /usr/sbin/avahi-daemon -k 2>/dev/null
    /usr/sbin/avahi-daemon --no-drop-root &
    service bluetooth restart
fi

/home/node/signalk/node_modules/signalk-server/bin/signalk-server --securityenabled


================================================
FILE: docker/v2_demo/Dockerfile
================================================
# docker buildx build --platform linux/amd64 -f  Dockerfile_heroku_api_demo -t registry.heroku.com/signalk-course-resources-api/web . && \
# docker push registry.heroku.com/signalk-course-resources-api/web && \
# heroku container:release web -a signalk-course-resources-api
FROM signalk/signalk-server:resources_course_api

USER root

WORKDIR /home/node/signalk
COPY startup_heroku_demo.sh startup.sh
RUN chmod +x startup.sh

COPY resources /home/node/.signalk/resources
COPY resources-provider.json /home/node/.signalk/plugin-config-data/
COPY course-data.json /home/node/.signalk/plugin-config-data/
COPY serverState /home/node/.signalk/serverState
RUN chown -R node /home/node/.signalk

USER node


================================================
FILE: docker/v2_demo/course-data.json
================================================
{
  "configuration": {
    "notifications": {},
    "calculations": {
      "method": "Rhumbline",
      "autopilot": true
    }
  },
  "enabled": true
}


================================================
FILE: docker/v2_demo/resources/routes/ad825f6c-1ae9-4f76-abc4-df2866b14b78
================================================
{"distance":18912,"name":"test route","description":"testing route stuff","feature":{"type":"Feature","geometry":{"type":"LineString","coordinates":[[23.421658428594455,59.976383142599445],[23.39545298552773,59.964698713370666],[23.386547033272887,59.94553321282956],[23.349311506736232,59.92852692137802],[23.352379069279134,59.912782827217114],[23.420858546854152,59.91443887159909],[23.529026801965298,59.9327648091369]]},"properties":{},"id":""}}

================================================
FILE: docker/v2_demo/resources/routes/da825f6c-1ae9-4f76-abc4-df2866b14b78
================================================
{"distance":18912,"name":"test route","description":"testing route stuff","feature":{"type":"Feature","geometry":{"type":"LineString","coordinates":[[23.421658428594455,59.976383142599445],[23.39545298552773,59.964698713370666],[23.386547033272887,59.94553321282956],[23.349311506736232,59.92852692137802],[23.352379069279134,59.912782827217114],[23.420858546854152,59.91443887159909],[23.529026801965298,59.9327648091369]]},"properties":{},"id":""}}

================================================
FILE: docker/v2_demo/resources/waypoints/ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a
================================================
{
    "name": "demo waypoint",
    "description": "",
    "feature": {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          23.455311064598344,
          59.99716209068623
        ]
      },
      "properties": {},
      "id": ""
    }
  }
  


================================================
FILE: docker/v2_demo/resources/waypoints/afe46290-aa98-4d2f-9c04-d199ca64942e
================================================
{
    "name": "lock",
    "description": "this is the lock",
    "feature": {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          23.435321561218167,
          59.98480312764812
        ]
      },
      "properties": {},
      "id": ""
    },
    "timestamp": "2022-04-21T18:23:19.815Z",
    "$source": "resources-provider"
  }

================================================
FILE: docker/v2_demo/resources-provider.json
================================================
{
  "configuration": {
    "standard": {
      "routes": true,
      "waypoints": true,
      "notes": true,
      "regions": true
    },
    "custom": [],
    "path": "./resources"
  }
}


================================================
FILE: docker/v2_demo/serverstate/course/settings.json
================================================
{
  "activeRoute": {
    "href": "/resources/routes/ad825f6c-1ae9-4f76-abc4-df2866b14b78",
    "startTime": "2022-04-21T18:40:44.319Z",
    "pointIndex": 3,
    "pointTotal": 7,
    "reverse": true
  },
  "nextPoint": {
    "href": null,
    "type": "RoutePoint",
    "position": {
      "latitude": 59.92852692137802,
      "longitude": 23.349311506736232
    },
    "arrivalCircle": 500
  },
  "previousPoint": {
    "href": null,
    "type": "RoutePoint",
    "position": {
      "longitude": 23.485033333333334,
      "latitude": 60.033516666666664
    }
  }
}


================================================
FILE: docker/v2_demo/startup_heroku_demo.sh
================================================
#!/usr/bin/env sh
service dbus restart
/usr/sbin/avahi-daemon -k
/usr/sbin/avahi-daemon --no-drop-root &
/home/node/signalk/bin/signalk-server --sample-nmea0183-data


================================================
FILE: docs/README.md
================================================
---
title: Introduction
---

# Introduction

Signal K Server is software designed to be deployed on a vessel to act as a central hub which:

1. Collects data from devices and sensors on board
1. Aggregates and exposes it using the _[Signal K Data Standard](https://signalk.org/specification/latest/)_
1. Exposes the collected data via REST APIs and websocket protocols over a standard WiFi, LAN or Internet connection.

Through implementation of the _[Signal K Data Standard](https://signalk.org/specification/latest/)_, it enables data exchange between NMEA0183, NMEA2000 and other marine protocols facilitating two way communication between the various onboard systems. In addition it can also act as data hub for additional sensors ensuring their data appears within the single data model. _(Visit the [Signal K SensESP project](https://github.com/SignalK/SensESP) for [ESP32](https://en.wikipedia.org/wiki/ESP32) for details.)._

Data is made available to client applications / connections in JSON format making it widely accessible to Apps on phone / tablet devices and web applications.

Signal K Server is also extensible, providing a plugin framework which allows developers to create solutions that integrate and extend its capabilities. These solutions can be published to **npmjs** and installed via the **App Store** in the server's web-based user interface.

![Server only setup](img/server_only.svg)


================================================
FILE: docs/breaking_changes.md
================================================
---
title: Breaking Changes
---

# Breaking Changes & Deprecations

This document lists breaking changes and deprecations in Signal K Server.

---

## Node.js: Default Version Updated to 24

Signal K Server now defaults to **Node.js 24** and requires **Node.js 22 or later**.

### What Changed

| Setting          | Before | After |
| ---------------- | ------ | ----- |
| Recommended      | 22     | 24    |
| Minimum required | 20     | 22    |

### Dropped Platform Support

Node.js 24 drops support for the following platforms:

- **armv7** (32-bit ARM) — Affects older Raspberry Pi models (Pi 2, Pi Zero/Zero W). Use a 64-bit OS on Pi 3/4/5 or stay on an older Signal K Server version.
- **Windows x86** (32-bit Windows) — Use 64-bit Windows instead.

### Action Required

- Update your Node.js installation to version 22 or later (version 24 recommended)
- If running on armv7 or Windows x86, you must migrate to a supported platform or remain on the previous Signal K Server version

---

## Admin UI: React 19 Migration

The Admin UI has been upgraded from React 16 to **React 19**. This is a significant update that may affect embedded webapps and plugin configuration panels.

### What Changed

| Component    | Before     | After      |
| ------------ | ---------- | ---------- |
| React        | 16.14.0    | 19.x       |
| React DOM    | 16.14.0    | 19.x       |
| React Router | 4.x        | 6.x        |
| Language     | JavaScript | TypeScript |

### Impact on Embedded Webapps

**If your webapp uses Module Federation to share React with the Admin UI:**

1. **Singleton sharing is now required** - Your webapp must configure React and ReactDOM as singletons with `requiredVersion: false`. See [vite.config.js](https://github.com/SignalK/signalk-server/blob/master/packages/server-admin-ui/vite.config.js) for the current configuration.

2. **React 19 compatibility** - If your webapp bundles its own React, it should be compatible with components rendered by the host. Most React 16/17/18 code works unchanged in React 19, but some deprecated APIs have been removed.

3. **String refs removed** - React 19 no longer supports string refs (`ref="myRef"`). Use `useRef()` instead.

4. **`defaultProps` on function components** - Deprecated. Use JavaScript default parameters instead.

### Impact on Plugin Configuration Panels

Plugin configuration panels using `./PluginConfigurationPanel` export continue to work. The props interface remains the same:

- `configuration` - the plugin's configuration data
- `save` - function to save configuration

### No Impact

- **Standalone webapps** - Webapps that don't use Module Federation sharing are not affected
- **Server APIs** - All Signal K HTTP and WebSocket APIs remain unchanged
- **Plugin JavaScript APIs** - Server-side plugin APIs are not affected

---

## Security: Anonymous Read Access Disabled by Default

When security is first enabled on a new installation, `allow_readonly` now defaults to `false`. Previously it defaulted to `true`, meaning anyone could read all Signal K data without authentication.

### Impact

- **New installations** will require authentication for all access, including read-only. Devices like chart plotters and instrument displays that previously worked without a token will need to be configured with access credentials.
- **Existing installations** are **not affected** — the `allow_readonly` value is already written explicitly in `security.json` and will be preserved.

### Mitigation

During initial security setup, the Enable Security dialog offers an **"Allow Readonly Access"** checkbox to opt in. Alternatively, you can enable it at any time in **Security > Settings**.

---

## REST API Changes

The following changes have been implemented with the introduction of **Resources API** and apply to applications using the `./signalk/v2/resources` endpoint.

_Note: These changes DO NOT impact applications using the `./signalk/v1/resources` endpoint._

### 1. Resource ID prefix assignment

The version 1 specification defined resource Ids with the following format `urn:mrn:signalk:uuid:<UUIDv4>`.

_e.g. `urn:mrn:signalk:uuid:18592f80-3425-43c2-937b-0be64b6be68c`_

The Resource API has dropped the use the prefix and ids are now just a uuidv4 value.

_e.g. `18592f80-3425-43c2-937b-0be64b6be68c`_

This format is used for both accessing a resource _e.g. `/signalk/v1/api/resources/waypoints/18592f80-3425-43c2-937b-0be64b6be68c`_ as well as the value within an `href` attribute.

_Example:_

```
{
   "name": "...",
   "descripton": "...",
   "href": "/resources/waypoints/18592f80-3425-43c2-937b-0be64b6be68c",
   ...
}
```

### 2. Resource Attributes

The Resources API has updated the definition of the following resources and may break applications that simply shift to using the `v2` api without catering for the changes:

- **routes**: removed the `start`, `end` properties.
- **waypoints**: removed `position` attribute, added `name`, `description` and `type` attributes.
- **regions**: removed `geohash` attribute, added `name` and `description` properties.
- **notes**: removed `geohash` and `region` attributes, added `href` and `properties` attributes.
- **charts**: There has been a significant changes to include support for WMS, WMTS and TileJSON sources.

Please see the [Resources OpenAPI definition](https://github.com/SignalK/signalk-server/blob/master/src/api/resources/openApi.json) for details.

---

## Deprecations:

### 1. courseGreatCircle, courseRhumbline paths

With the introduction of the Course API the following paths should now be considered deprecated:

- `/signalk/v1/api/vessels/self/navigation/courseGreatCircle`
- `/signalk/v1/api/vessels/self/navigation/courseRhumbline`

_Note: The Course API does currently maintain values in these paths for the purposes of backward compatibility, but applications and plugins referencing these paths should plan to move to using the equivalent paths under `/signalk/v2/api/vessels/self/navigation/course`._


================================================
FILE: docs/develop/README.md
================================================
---
title: Developing
children:
  - ../whats_new.md
  - ../breaking_changes.md
  - plugins/README.md
  - rest-api/README.md
  - ../guides/unitpreferences.md
---

# Developing with Signal K

Signal K Server has an extensible architecture that enables developers to add functionality to support new protocols, devices, information sources, etc.

The information in this section aims to provide guidance, not only on how to develop plugins and applications to extend capability, but also how to do so in alignment with the Signal K specification, protocol and server architecture.
By understanding the underlying architecture, the plugins and apps you create will ensure that the additional functionality and data will be discoverable and work in harmony with other solutions.

See also the [contributing document](https://github.com/SignalK/signalk-server/blob/master/contributing.md) for instructions on contributing to the server's code.

## Looking Ahead

Signal K Server v2 marks the start of an evolution from the Signal K v1 approach of defining the paths, their hierarchy and the full data model schema, towards an approach centered around modular REST APIs (HTTP endpoints defined as OpenApi specifications).

These APIs enact operations (i.e. activate a route, advance to next point, etc) rather just expose a generic data model with some well known paths.
They are available under the path `/signalk/v2/api` so they can coexist with v1 APIs. There is a connection with the Signal K full data model but, unlike the v1 APIs it is not 1:1, it is abstracted behind the interface.

The reason for adopting this approach is to address the fact that many paths within a Signal K hierarchy are related, a change in the value of one path will require that the value of other paths be updated to ensure that the data model is consistent.
At present this relies on the plugin / application knowing which paths in the hierarchy are related. Additionally there may be other plugins / applications also updating some of the same paths which can cause the data model to become invalid, which then erodes trust in the data which impacts its use in navigation.

The v1 model for using PUT handlers is also very vague and causes confusion. The aim of defining APIs with clear contracts using industry standard OpenApi mechanism is to make APIs discoverable and their use and semantics explicit.

The use of APIs to perform operations addresses these issues providing the following benefits:

1. A standardised interface for all applications / plugins to perform an operation
1. Provides clear ownership of the paths in the Signal K data model
1. Ensures values are being maintained in all of the related paths.
1. Increases trust in the data for use in all scenarios.

### Stream Interface

Currently, when v2 REST APIs emit deltas that contain v2 paths and structure, but they do not end up in the full model. This means that these paths and values are only available via API GET requests.

## Offline Use

When operating on a vessel you should not assume that a connection to Internet services is available.
Therefore, it is important that the WebApps and Plugins you create be _"self contained"_ and provide all the resources they require to operate _(i.e. fonts, stylesheets, images, etc)_. This also minimises data charges even if your module does use data over Internet.

For WebApps and Plugins that do connect to Internet based services to provide data, they should be resilient to changes in the connection status to those services and where necessary display their status.

## Deprecations and Breaking Changes

With the move towards REST APIs and the desire to improve the data model (and also fix some mistakes) it's inevitable that there will be deprecations and breaking changes.

For example, when addressing duplicate Great Circle and Rhumbline course paths, the resultant changes will likley break compatibility with v1.

For details about paths that are flagged for deprecation see [Changes & Deprecations](../breaking_changes.md).


================================================
FILE: docs/develop/plugins/README.md
================================================
---
title: Plugins
children:
  - ../webapps.md
  - wasm/README.md
  - deltas.md
  - configuration.md
  - backpressure.md
  - autopilot_provider_plugins.md
  - course_calculations.md
  - resource_provider_plugins.md
  - weather_provider_plugins.md
  - custom_renderers.md
  - publishing.md
  - ci.md
  - release.md
---

# Server plugins

Signal K Node server plugins are components that extend functionality of the server.
They are installed via the AppStore and configured via the Admin UI.

Signal K server exposes an interface for plugins to use in order to interact with the full data model, emit delta messages and process requests.

## Plugin Types

Signal K supports two types of plugins:

- **JavaScript Plugins** - Traditional JavaScript/TypeScript plugins (documented below)
- **[WASM Plugins](./wasm/README.md)** - Plugins written in Rust, AssemblyScript, Go, or other WASM-compatible languages

[WASM](https://en.wikipedia.org/wiki/WebAssembly) is short for WebAssembly. WASM is a runtime for executing portable code in near native speeds and in isolation. WASM plugins offer sandbox isolation, memory safety, and the ability to use languages other than JavaScript. See the [WASM Plugins documentation](./wasm/README.md) for details.

## Node.js Plugin Capabilities

Plugins can:

- Expose _[REST APIs](../rest-api/README.md)_ to provide consumers/clients a way to perform operations offered by your plugin. The APIs will be published under `http://{skserver}:3000/plugins/{pluginId}`.

- Provide a webapp by placing the relevant files in a folder named `/public/` which the server will mount under `http://{skserver}:3000/{pluginId}`.

**Note: With the move towards Signal K server providing APIs to perform operations, it is important that you consider how the proposed functionality provided by your plugin aligns with the Signal K architecture before starting development.**

For example, if the plugin you are looking to develop is providing access to information such as `route,` `waypoint`, `POI`, or `charts` you should be creating a _[Resources Provider Plugin](./resource_provider_plugins.md)_ for the _[Resources API](../rest-api/resources_api.md)_.

Or if you are looking to perform course calculations or integrate with an autopilot, you will want to review the _[Course API](../rest-api/course_api.md)_ documentation prior to commencing your project.

**OpenApi description for your plugin's API**

If your plugin provides an API you should consider providing an OpenApi description. This promotes cooperation with other plugin/webapp authors and also paves the way for incorporating new APIs piloted within a plugin into the Signal K specification. _See [Add OpenAPI definition](#add-an-openapi-definition)_ below.

---

## Getting Started with Plugin Development

### Prerequisites

To get started developing your plugin you will need the following:

- Signal K server instance on your device _(clone of GIT repository or docker instance)_
- NodeJs version 20 or later and NPM installed
- SignalK server configuration folder. _(Created when Signal K server is started. default location is `$HOME/.signalk`)_.

---

### Setting up your project

1. Create a folder for your plugin code and create the necessary file structure:

```shell
mkdir my-plugin
cd my-plugin
npm init      # create package.json file
```

2. Create the folders to hold your plugin code and webapp UI.

```shell
/my-plugin
  /plugin     # plugin (javascript code / built typesrcipt code)
    index.js
    ..
  /public     # web app UI
    index.html
    ..
  /src        # typescript source code (not required if using javascript)
    index.ts
    ...
  package.json
```

3. Update the `package.json` to reflect your project structure and add keywords to identify the package for the Signal K AppStore.

```JSON
{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "My signalk plugin",
  "keywords": [
    "signalk-node-server-plugin",
    "signalk-category-ais"
  ],
  "signalk-plugin-enabled-by-default": false,
  "signalk": {
    "appIcon": "./assets/icons/icon-72x72.png",
    "displayName": "My Great WebApp"
  },
  "main": "plugin/index.js",
  ...
}
```

4. _Optional:_ Install any dependencies or third party packages.

```shell
npm i
```

### Link your project to Signal K server.

Once you have developed your plugin code and are ready to debug, the most convenient way is to use `npm link` to link your plugin code to your instance of Signal K server.

To do this, from within a terminal window (if you are using Docker, the following must be executed from the container terminal):

```shell
# Ensure you are in the folder containing your built plugin code
cd my_plugin_src

# Create a link (may require the use of sudo)
npm link

# Change to the Signal K server configuration directory
cd ~/.signalk

# Link your plugin using the name in the package.json file
#(may require the use of sudo)
npm link my-signalk-plugin-app
```

When you start Signal K server the plugin will now appear in the **Plugin Config** screen where it can be configured and enabled.

Updating and/or installing new plugins will remove the link and you need to re-link your plugin.

### Debugging

The simplest way to debug your plugin is to turn on **Enable Debug log** for your plugin in the **Plugin Config** screen.

Alternatively, you can debug your plugin by starting the Signal K server with the `DEBUG` environment variable:

```shell
$ DEBUG=my-signalk-plugin signalk-server

# sample output
my-signalk-plugin Plugin stopped +0ms
my-signalk-plugin Plugin started +2ms
```

You can also view debug information about the plugin loading process:

```shell
$ DEBUG=signalk:interfaces:plugins signalk-server

# sample output
signalk:interfaces:plugins Registering plugin my-signalk-plugin +0ms
signalk:interfaces:plugins Could not find options for plugin my-signalk-plugin, returning empty options:  +2ms
```

#### Sample Data

For development purposes, it's often nice to have some mocked data. SignalK comes with a synthesized NMEA2000 data set that can be used as sample data.

You can enable this by adding `--sample-n2k-data` to the command line:

```shell
$ DEBUG=my-signalk-plugin signalk-server --sample-n2k-data
```

---

## Start Coding

Signal K server plugins are NodeJs `javascript` or `typescript` projects that return an object that implements the {@link @signalk/server-api!Plugin | Plugin} interface.

They are installed into the `node_modules` folder that resides inside the SignalK server's configuration directory _(`$HOME/.signalk` by default)_.

A Signal K plugin is passed a reference to the Signal K server plugin interface which it can use to interact with the server.

Following are code snippets that can be used as a template for plugin development ensuring the returned Plugin object contains the required functions.

### Javascript

Create `index.js` with the following content:

```javascript
module.exports = (app) => {
  const plugin = {
    id: 'my-signalk-plugin',
    name: 'My Great Plugin',
    start: (settings, restartPlugin) => {
      // start up code goes here.
    },
    stop: () => {
      // shutdown code goes here.
    },
    schema: () => {
      properties: {
        // plugin configuration goes here
      }
    }
  }

  return plugin
}
```

### Typescript

Create `index.js` with the following content:

```typescript
import { Plugin, ServerAPI } from '@signalk/server-api'

const start = (app: ServerAPI): Plugin => {
  const plugin: Plugin = {
    id: 'my-signalk-plugin',
    name: 'My Great Plugin',
    start: (settings, restartPlugin) => {
      // start up code goes here.
    },
    stop: () => {
      // shutdown code goes here.
    },
    schema: () => {
      properties: {
        // plugin configuration goes here
      }
    }
  }

  return plugin
}
module.exports = start
```

A plugin must return an object containing the following functions:

- `start(settings, restartPlugin)`: This function is called when the plugin is enabled or when the server starts (and the plugin is enabled). The `settings` parameter contains the configuration data entered via the **Plugin Config** screen. `restartPlugin` is a function that can be called by the plugin to restart itself.

- `stop()`: This function is called when the plugin is disabled or after configuration changes. Use this function to "clean up" the resources consumed by the plugin i.e. unsubscribe from streams, stop timers / loops and close devices.
  If there are asynchronous operations in your plugin's stop implementation you should return a Promise that resolves
  when stopping is complete.

- `schema()`: A function that returns an object defining the schema of the plugin's configuration data. It is used by the server to generate the user interface in the **Plugin Config** screen.

_Note: When a plugin's configuration is changed the server will first call `stop()` to stop the plugin and then `start()` with the new configuration data. Return a Promise from `stop` if needed so that `start` is not called before stopping is complete._

A plugin can also contain the following optional functions:

- `uiSchema()`: A function that returns an object defining the attributes of the UI components displayed in the **Plugin Config** screen.

- `registerWithRouter(router)`: This function (which is called during plugin startup) enables plugins to provide an API (REST API in our context) by registering paths with the Express router. The APIs will be published under `http://{skserver}:3000/plugins/{pluginId}{path string}`. It is strongly recommended that the plugin implement `getOpenAPI()` to publish API documentation if this function is used.

_Example:_

```javascript
plugin.registerWithRouter = (router) => {
  router.get('/preferences', (req, res) => {
    // URL will be http://{skserver}:3000/plugins/{pluginId}/preferences
    res.status(200).json({
      preferences: {
        color
Download .txt
gitextract_s_vrwrw7/

├── .coderabbit.yaml
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-base-image.yml
│       ├── build-docker.yml
│       ├── plugin-ci.yml
│       ├── release.yml
│       ├── require_pr_label.yml
│       ├── security-scan.yml
│       └── test.yml
├── .gitignore
├── .mocharc.js
├── .npmignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc.json
├── .python-version
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE
├── Procfile
├── README.md
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile_base_24.04
│   ├── Dockerfile_base_alpine
│   ├── Dockerfile_rel
│   ├── README.md
│   ├── avahi/
│   │   └── avahi-dbus.conf
│   ├── bluez/
│   │   └── bluezuser.conf
│   ├── docker-compose.yml
│   ├── startup.sh
│   └── v2_demo/
│       ├── Dockerfile
│       ├── course-data.json
│       ├── resources/
│       │   ├── routes/
│       │   │   ├── ad825f6c-1ae9-4f76-abc4-df2866b14b78
│       │   │   └── da825f6c-1ae9-4f76-abc4-df2866b14b78
│       │   └── waypoints/
│       │       ├── ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a
│       │       └── afe46290-aa98-4d2f-9c04-d199ca64942e
│       ├── resources-provider.json
│       ├── serverstate/
│       │   └── course/
│       │       └── settings.json
│       └── startup_heroku_demo.sh
├── docs/
│   ├── README.md
│   ├── breaking_changes.md
│   ├── develop/
│   │   ├── README.md
│   │   ├── plugins/
│   │   │   ├── README.md
│   │   │   ├── autopilot_provider_plugins.md
│   │   │   ├── backpressure.md
│   │   │   ├── ci.md
│   │   │   ├── configuration.md
│   │   │   ├── course_calculations.md
│   │   │   ├── custom_renderers.md
│   │   │   ├── deltas.md
│   │   │   ├── examples/
│   │   │   │   ├── plugin-caller-example.yml
│   │   │   │   ├── plugin-dependabot-example.yml
│   │   │   │   └── plugin-release-example.yml
│   │   │   ├── publishing.md
│   │   │   ├── release.md
│   │   │   ├── resource_provider_plugins.md
│   │   │   ├── wasm/
│   │   │   │   ├── README.md
│   │   │   │   ├── assemblyscript.md
│   │   │   │   ├── best_practices.md
│   │   │   │   ├── capabilities.md
│   │   │   │   ├── deltas.md
│   │   │   │   ├── go.md
│   │   │   │   ├── http_endpoints.md
│   │   │   │   ├── integration_guide.md
│   │   │   │   └── rust.md
│   │   │   └── weather_provider_plugins.md
│   │   ├── rest-api/
│   │   │   ├── README.md
│   │   │   ├── autopilot_api.md
│   │   │   ├── conventions.md
│   │   │   ├── course_api.md
│   │   │   ├── history_api.md
│   │   │   ├── notifications_api.md
│   │   │   ├── plugin_api.md
│   │   │   ├── proposed/
│   │   │   │   ├── README.md
│   │   │   │   └── anchor_api.md
│   │   │   ├── radar_api.md
│   │   │   ├── resources_api.md
│   │   │   └── weather_api.md
│   │   └── webapps.md
│   ├── guides/
│   │   ├── README.md
│   │   ├── anchoralarm/
│   │   │   └── anchoralarm.md
│   │   ├── datalogging/
│   │   │   └── datalogging.md
│   │   ├── navdataserver/
│   │   │   └── navdataserver.md
│   │   ├── udev.md
│   │   └── unitpreferences.md
│   ├── img/
│   │   ├── autopilot_provider.dia
│   │   ├── course_provider.dia
│   │   ├── notification_manager.dia
│   │   ├── resource_provider.dia
│   │   └── server_only.dia
│   ├── installation/
│   │   ├── README.md
│   │   ├── command_line.md
│   │   ├── docker.md
│   │   ├── npm.md
│   │   ├── raspberry_pi_installation.md
│   │   ├── source.md
│   │   └── updating.md
│   ├── internal/
│   │   ├── README.md
│   │   ├── wasm-architecture.md
│   │   └── wasm-asyncify.md
│   ├── oidc.md
│   ├── security-architecture.md
│   ├── security.md
│   ├── setup/
│   │   ├── configuration.md
│   │   ├── generating_tokens.md
│   │   ├── nmea.md
│   │   └── seatalk/
│   │       └── README.md
│   ├── src/
│   │   └── features/
│   │       └── weather/
│   │           └── weather.md
│   ├── support/
│   │   ├── help.md
│   │   └── sponsor.md
│   └── whats_new.md
├── empty_file
├── eslint.config.js
├── examples/
│   └── wasm-plugins/
│       ├── example-anchor-watch-rust/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── Cargo.toml
│       │   ├── README.md
│       │   ├── package.json
│       │   ├── src/
│       │   │   └── lib.rs
│       │   └── wit/
│       │       └── signalk-plugin.wit
│       ├── example-hello-assemblyscript/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── README.md
│       │   ├── asconfig.json
│       │   ├── assembly/
│       │   │   └── index.ts
│       │   └── package.json
│       ├── example-routes-waypoints/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── README.md
│       │   ├── asconfig.json
│       │   ├── assembly/
│       │   │   └── index.ts
│       │   └── package.json
│       ├── example-weather-plugin/
│       │   ├── .gitignore
│       │   ├── .npmignore
│       │   ├── README.md
│       │   ├── asconfig.json
│       │   ├── assembly/
│       │   │   └── index.ts
│       │   └── package.json
│       └── example-weather-provider/
│           ├── .gitignore
│           ├── .npmignore
│           ├── README.md
│           ├── asconfig.json
│           ├── assembly/
│           │   └── index.ts
│           └── package.json
├── fly_io/
│   ├── cr_signalk_io/
│   │   ├── Dockerfile
│   │   ├── fly.toml
│   │   └── nginx.conf
│   └── demo_signalk_org/
│       ├── Dockerfile
│       ├── fly.toml
│       └── security.json
├── index.js
├── kubernetes/
│   ├── README.md
│   ├── signalk-deployment.yaml
│   └── signalk-ingress.yaml
├── kubernetes.md
├── package.json
├── packages/
│   ├── assemblyscript-plugin-sdk/
│   │   ├── .npmignore
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── asconfig.json
│   │   ├── assembly/
│   │   │   ├── api.ts
│   │   │   ├── index.ts
│   │   │   ├── network.ts
│   │   │   ├── plugin.ts
│   │   │   ├── resources.ts
│   │   │   └── signalk.ts
│   │   ├── build/
│   │   │   ├── plugin.d.ts
│   │   │   └── plugin.js
│   │   └── package.json
│   ├── resources-provider-plugin/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── @types/
│   │   │   │   └── geojson-validation.d.ts
│   │   │   ├── index.ts
│   │   │   ├── openApi.json
│   │   │   └── types/
│   │   │       ├── index.ts
│   │   │       └── store.ts
│   │   └── tsconfig.json
│   ├── server-admin-ui/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── README.md
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public_src/
│   │   │   └── index.html
│   │   ├── scss/
│   │   │   ├── _bootstrap-variables.scss
│   │   │   ├── _core-variables.scss
│   │   │   ├── _custom.scss
│   │   │   ├── core/
│   │   │   │   ├── _animate.scss
│   │   │   │   ├── _aside.scss
│   │   │   │   ├── _avatars.scss
│   │   │   │   ├── _badge.scss
│   │   │   │   ├── _breadcrumb-menu.scss
│   │   │   │   ├── _breadcrumb.scss
│   │   │   │   ├── _buttons.scss
│   │   │   │   ├── _callout.scss
│   │   │   │   ├── _card.scss
│   │   │   │   ├── _charts.scss
│   │   │   │   ├── _dropdown-menu-right.scss
│   │   │   │   ├── _dropdown.scss
│   │   │   │   ├── _footer.scss
│   │   │   │   ├── _grid.scss
│   │   │   │   ├── _input-group.scss
│   │   │   │   ├── _layout.scss
│   │   │   │   ├── _loading.scss
│   │   │   │   ├── _mixins.scss
│   │   │   │   ├── _mobile.scss
│   │   │   │   ├── _modal.scss
│   │   │   │   ├── _nav.scss
│   │   │   │   ├── _navbar.scss
│   │   │   │   ├── _others.scss
│   │   │   │   ├── _progress.scss
│   │   │   │   ├── _rtl.scss
│   │   │   │   ├── _sidebar.scss
│   │   │   │   ├── _switches.scss
│   │   │   │   ├── _tables.scss
│   │   │   │   ├── _temp.scss
│   │   │   │   ├── _typography.scss
│   │   │   │   ├── _utilities.scss
│   │   │   │   ├── _variables.scss
│   │   │   │   ├── _widgets.scss
│   │   │   │   ├── core.scss
│   │   │   │   └── utilities/
│   │   │   │       ├── _background.scss
│   │   │   │       ├── _borders.scss
│   │   │   │       └── _display.scss
│   │   │   ├── style.scss
│   │   │   └── vendors/
│   │   │       ├── _variables.scss
│   │   │       └── chart.js/
│   │   │           └── chart.scss
│   │   ├── src/
│   │   │   ├── actions.ts
│   │   │   ├── blinking-circle.css
│   │   │   ├── bootstrap.tsx
│   │   │   ├── components/
│   │   │   │   ├── Aside/
│   │   │   │   │   └── Aside.tsx
│   │   │   │   ├── Footer/
│   │   │   │   │   └── Footer.tsx
│   │   │   │   ├── Header/
│   │   │   │   │   └── Header.tsx
│   │   │   │   ├── Icons.tsx
│   │   │   │   ├── Sidebar/
│   │   │   │   │   └── Sidebar.tsx
│   │   │   │   ├── SidebarFooter/
│   │   │   │   │   └── SidebarFooter.tsx
│   │   │   │   ├── SidebarForm/
│   │   │   │   │   └── SidebarForm.tsx
│   │   │   │   ├── SidebarHeader/
│   │   │   │   │   └── SidebarHeader.tsx
│   │   │   │   └── SidebarMinimizer/
│   │   │   │       └── SidebarMinimizer.tsx
│   │   │   ├── containers/
│   │   │   │   └── Full/
│   │   │   │       └── Full.tsx
│   │   │   ├── contexts/
│   │   │   │   └── WebSocketContext.tsx
│   │   │   ├── dataFetching.ts
│   │   │   ├── dependency-sync.test.ts
│   │   │   ├── fa-pulse.css
│   │   │   ├── hooks/
│   │   │   │   └── useWebSocket.ts
│   │   │   ├── index.ts
│   │   │   ├── routes.ts
│   │   │   ├── services/
│   │   │   │   └── WebSocketService.ts
│   │   │   ├── store/
│   │   │   │   ├── index.ts
│   │   │   │   ├── slices/
│   │   │   │   │   ├── appSlice.test.ts
│   │   │   │   │   ├── appSlice.ts
│   │   │   │   │   ├── dataSlice.test.ts
│   │   │   │   │   ├── dataSlice.ts
│   │   │   │   │   ├── prioritiesSlice.test.ts
│   │   │   │   │   ├── prioritiesSlice.ts
│   │   │   │   │   ├── unitPreferencesSlice.ts
│   │   │   │   │   ├── wsSlice.test.ts
│   │   │   │   │   └── wsSlice.ts
│   │   │   │   └── types.ts
│   │   │   ├── test/
│   │   │   │   └── setup.ts
│   │   │   ├── types/
│   │   │   │   └── jsonlint-mod.d.ts
│   │   │   ├── utils/
│   │   │   │   └── unitConversion.ts
│   │   │   └── views/
│   │   │       ├── Configuration/
│   │   │       │   ├── Configuration.tsx
│   │   │       │   └── EmbeddedPluginConfigurationForm.tsx
│   │   │       ├── Dashboard/
│   │   │       │   └── Dashboard.tsx
│   │   │       ├── DataBrowser/
│   │   │       │   ├── CopyToClipboardWithFade.tsx
│   │   │       │   ├── DataBrowser.tsx
│   │   │       │   ├── DataRow.tsx
│   │   │       │   ├── GranularSubscriptionManager.ts
│   │   │       │   ├── Meta.tsx
│   │   │       │   ├── TimestampCell.tsx
│   │   │       │   ├── ValueRenderers.tsx
│   │   │       │   ├── VirtualTable.css
│   │   │       │   ├── VirtualizedDataTable.tsx
│   │   │       │   ├── VirtualizedMetaTable.tsx
│   │   │       │   ├── pathUtils.ts
│   │   │       │   └── usePathData.ts
│   │   │       ├── Playground.tsx
│   │   │       ├── ServerConfig/
│   │   │       │   ├── BackupRestore.tsx
│   │   │       │   ├── BasicProvider.tsx
│   │   │       │   ├── Logging.tsx
│   │   │       │   ├── N2KFilters.tsx
│   │   │       │   ├── PluginConfigurationForm.tsx
│   │   │       │   ├── ProvidersConfiguration.tsx
│   │   │       │   ├── ServerLog.tsx
│   │   │       │   ├── ServerUpdate.tsx
│   │   │       │   ├── Settings.tsx
│   │   │       │   ├── SourcePriorities.tsx
│   │   │       │   ├── UnitPreferencesSettings.tsx
│   │   │       │   └── VesselConfiguration.tsx
│   │   │       ├── Webapps/
│   │   │       │   ├── Embedded.tsx
│   │   │       │   ├── EmbeddedAsyncApi.tsx
│   │   │       │   ├── EmbeddedDocs.tsx
│   │   │       │   ├── Webapp.tsx
│   │   │       │   ├── Webapps.tsx
│   │   │       │   ├── dynamicutilities.ts
│   │   │       │   └── loadingerror.tsx
│   │   │       ├── appstore/
│   │   │       │   ├── Apps/
│   │   │       │   │   ├── Apps.tsx
│   │   │       │   │   └── WarningBox.tsx
│   │   │       │   ├── AppsList.tsx
│   │   │       │   ├── Grid/
│   │   │       │   │   └── cell-renderers/
│   │   │       │   │       ├── ActionCellRenderer.test.tsx
│   │   │       │   │       └── ActionCellRenderer.tsx
│   │   │       │   └── appStore.scss
│   │   │       └── security/
│   │   │           ├── AccessRequests.tsx
│   │   │           ├── Devices.tsx
│   │   │           ├── EnableSecurity.tsx
│   │   │           ├── Login.tsx
│   │   │           ├── OIDCSettings.tsx
│   │   │           ├── Register.tsx
│   │   │           ├── Settings.tsx
│   │   │           └── Users.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── server-admin-ui-dependencies/
│   │   ├── index.js
│   │   └── package.json
│   ├── server-api/
│   │   ├── .gitignore
│   │   ├── .npmignore
│   │   ├── .npmrc
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── autopilotapi.ts
│   │   │   ├── brand.ts
│   │   │   ├── course.ts
│   │   │   ├── coursetypes.ts
│   │   │   ├── deltas.test.ts
│   │   │   ├── deltas.ts
│   │   │   ├── features.ts
│   │   │   ├── history.ts
│   │   │   ├── index.ts
│   │   │   ├── mmsi/
│   │   │   │   ├── mid.ts
│   │   │   │   ├── mmsi.test.ts
│   │   │   │   └── mmsi.ts
│   │   │   ├── notificationsapi.ts
│   │   │   ├── plugin.ts
│   │   │   ├── propertyvalues.test.ts
│   │   │   ├── propertyvalues.ts
│   │   │   ├── radarapi.ts
│   │   │   ├── resourcesapi.ts
│   │   │   ├── resourcetypes.ts
│   │   │   ├── serverapi.ts
│   │   │   ├── streambundle.ts
│   │   │   ├── subscriptionmanager.ts
│   │   │   ├── typebox/
│   │   │   │   ├── autopilot-schemas.ts
│   │   │   │   ├── course-schemas.ts
│   │   │   │   ├── discovery-schemas.ts
│   │   │   │   ├── history-schemas.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── notifications-schemas.ts
│   │   │   │   ├── protocol-schemas.ts
│   │   │   │   ├── radar-schemas.ts
│   │   │   │   ├── resources-schemas.ts
│   │   │   │   ├── shared-schemas.ts
│   │   │   │   └── weather-schemas.ts
│   │   │   ├── weatherapi.guard.ts
│   │   │   └── weatherapi.ts
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   └── wit/
│   │       └── signalk.wit
│   ├── streams/
│   │   ├── .npmrc
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── actisense-serial.ts
│   │   │   ├── autodetect.test.ts
│   │   │   ├── autodetect.ts
│   │   │   ├── canboatjs.test.ts
│   │   │   ├── canboatjs.ts
│   │   │   ├── canbus.ts
│   │   │   ├── execute.test.ts
│   │   │   ├── execute.ts
│   │   │   ├── filestream.ts
│   │   │   ├── folderstream.ts
│   │   │   ├── from_json.test.ts
│   │   │   ├── from_json.ts
│   │   │   ├── gpiod-seatalk.ts
│   │   │   ├── gpsd.test.ts
│   │   │   ├── gpsd.ts
│   │   │   ├── index.ts
│   │   │   ├── keys-filter.test.ts
│   │   │   ├── keys-filter.ts
│   │   │   ├── liner.test.ts
│   │   │   ├── liner.ts
│   │   │   ├── log.ts
│   │   │   ├── logging.test.ts
│   │   │   ├── logging.ts
│   │   │   ├── mdns-ws.test.ts
│   │   │   ├── mdns-ws.ts
│   │   │   ├── multiplexedlog.ts
│   │   │   ├── n2k-signalk.test.ts
│   │   │   ├── n2k-signalk.ts
│   │   │   ├── n2kAnalyzer.ts
│   │   │   ├── nmea0183-signalk.test.ts
│   │   │   ├── nmea0183-signalk.ts
│   │   │   ├── nullprovider.ts
│   │   │   ├── pigpio-seatalk.ts
│   │   │   ├── replacer.test.ts
│   │   │   ├── replacer.ts
│   │   │   ├── s3.ts
│   │   │   ├── serialport.ts
│   │   │   ├── simple.ts
│   │   │   ├── splitting-liner.test.ts
│   │   │   ├── splitting-liner.ts
│   │   │   ├── tcp.test.ts
│   │   │   ├── tcp.ts
│   │   │   ├── tcpserver.ts
│   │   │   ├── test-helpers.ts
│   │   │   ├── throttle.ts
│   │   │   ├── timestamp-throttle.test.ts
│   │   │   ├── timestamp-throttle.ts
│   │   │   ├── types.ts
│   │   │   ├── udp.test.ts
│   │   │   ├── udp.ts
│   │   │   └── vendor.d.ts
│   │   └── tsconfig.json
│   └── typedoc-theme/
│       ├── .gitignore
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       ├── src/
│       │   ├── SignalKTheme.tsx
│       │   ├── SignalKThemeContext.tsx
│       │   ├── assets/
│       │   │   └── theme.css
│       │   ├── index.tsx
│       │   └── partials/
│       │       └── toolbar.tsx
│       └── tsconfig.json
├── public/
│   └── examples/
│       ├── http-example.html
│       ├── index.html
│       └── loginform.html
├── releasing.md
├── samples/
│   ├── aava-n2k.data
│   ├── gofree-merrimac.log
│   ├── gps.log
│   ├── n2kd-183-merrimac.log
│   ├── nais300-merrimac.log
│   ├── nais400-merrimac.log
│   └── plaka.log
├── settings/
│   ├── actisense-serial-settings.json
│   ├── commandline-provider-settings.json
│   ├── defaults.json-sample
│   ├── multiple-sources.json
│   ├── multiplexed.json
│   ├── n2k-from-file-settings.json
│   ├── signalk-ws-settings.json
│   ├── simulator.json
│   ├── volare-file-settings-filtered.json
│   ├── volare-file-settings.json
│   ├── volare-gpsd-settings.json
│   ├── volare-serial-settings.json
│   ├── volare-tcp-settings.json
│   └── volare-udp-settings.json
├── src/
│   ├── @types/
│   │   ├── api-schema-builder.d.ts
│   │   ├── primus.d.ts
│   │   └── signalk_signalk-schema.d.ts
│   ├── BackpressureManager.ts
│   ├── LatestValuesAccumulator.ts
│   ├── api/
│   │   ├── apps/
│   │   │   ├── openApi.json
│   │   │   └── openApi.ts
│   │   ├── autopilot/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── course/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── discovery/
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── history/
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── index.ts
│   │   ├── notifications/
│   │   │   ├── alarm.ts
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   ├── notificationManager.ts
│   │   │   └── openApi.ts
│   │   ├── openApiSchemas.ts
│   │   ├── radar/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   └── openApi.ts
│   │   ├── resources/
│   │   │   ├── asyncApi.ts
│   │   │   ├── index.ts
│   │   │   ├── openApi.ts
│   │   │   └── validate.ts
│   │   ├── security/
│   │   │   ├── openApi.json
│   │   │   └── openApi.ts
│   │   ├── streams/
│   │   │   ├── binary-stream-manager.ts
│   │   │   └── index.ts
│   │   ├── swagger.ts
│   │   └── weather/
│   │       ├── index.ts
│   │       └── openApi.ts
│   ├── app.ts
│   ├── atomicWrite.ts
│   ├── baconjs-compat.ts
│   ├── categories.ts
│   ├── config/
│   │   ├── config.test.js
│   │   ├── config.ts
│   │   ├── development.js
│   │   ├── get.js
│   │   └── production.js
│   ├── constants.ts
│   ├── cors.ts
│   ├── debug.ts
│   ├── deltaPriority.ts
│   ├── deltacache.ts
│   ├── deltachain.ts
│   ├── deltaeditor.ts
│   ├── deltastats.ts
│   ├── discovery.js
│   ├── dummysecurity.ts
│   ├── events.ts
│   ├── index.ts
│   ├── interfaces/
│   │   ├── applicationData.js
│   │   ├── appstore.js
│   │   ├── index.js
│   │   ├── logfiles.js
│   │   ├── mfd_webapp.ts
│   │   ├── nmea-tcp.ts
│   │   ├── playground.js
│   │   ├── plugins.ts
│   │   ├── providers.ts
│   │   ├── rest.js
│   │   ├── tcp.ts
│   │   ├── unitpreferences-api.js
│   │   ├── wasm.ts
│   │   ├── webapps.js
│   │   └── ws.ts
│   ├── logging.js
│   ├── login-rate-limiter.ts
│   ├── mdns.js
│   ├── modules.ts
│   ├── oidc/
│   │   ├── authorization.ts
│   │   ├── config.ts
│   │   ├── discovery.ts
│   │   ├── id-token-validation.ts
│   │   ├── index.ts
│   │   ├── oidc-admin.ts
│   │   ├── oidc-auth.ts
│   │   ├── permission-mapping.ts
│   │   ├── pkce.ts
│   │   ├── state.ts
│   │   ├── token-exchange.ts
│   │   ├── types.ts
│   │   └── user-info.ts
│   ├── pipedproviders.ts
│   ├── plugin-paths.ts
│   ├── pluginid.ts
│   ├── ports.ts
│   ├── put.ts
│   ├── redirects.json
│   ├── requestResponse.ts
│   ├── security.ts
│   ├── serialports.ts
│   ├── serverroutes.ts
│   ├── serverstate/
│   │   └── store.ts
│   ├── streambundle.ts
│   ├── subscriptionmanager.ts
│   ├── tokensecurity.ts
│   ├── types.ts
│   ├── unitpreferences/
│   │   ├── index.ts
│   │   ├── loader.ts
│   │   ├── resolver.ts
│   │   └── types.ts
│   ├── version.ts
│   ├── wasm/
│   │   ├── bindings/
│   │   │   ├── binary-stream.ts
│   │   │   ├── env-imports.ts
│   │   │   ├── index.ts
│   │   │   ├── radar-provider.ts
│   │   │   ├── resource-provider.ts
│   │   │   ├── socket-manager.ts
│   │   │   └── weather-provider.ts
│   │   ├── index.ts
│   │   ├── loader/
│   │   │   ├── index.ts
│   │   │   ├── plugin-config.ts
│   │   │   ├── plugin-lifecycle.ts
│   │   │   ├── plugin-registry.ts
│   │   │   ├── plugin-routes.ts
│   │   │   └── types.ts
│   │   ├── loaders/
│   │   │   ├── index.ts
│   │   │   └── standard-loader.ts
│   │   ├── types.ts
│   │   ├── utils/
│   │   │   ├── fetch-wrapper.ts
│   │   │   ├── format-detection.ts
│   │   │   └── index.ts
│   │   ├── wasm-runtime.ts
│   │   ├── wasm-serverapi.ts
│   │   ├── wasm-storage.ts
│   │   └── wasm-subscriptions.ts
│   ├── zip.ts
│   └── zones.ts
├── test/
│   ├── BackpressureManager.ts
│   ├── LatestValuesAccumulator.js
│   ├── acls.js
│   ├── applicationData.ts
│   ├── chart-tile-regex.ts
│   ├── course.ts
│   ├── delete.js
│   ├── deltaPriority.ts
│   ├── deltacache.js
│   ├── endpoint-auth.ts
│   ├── error-logging.ts
│   ├── externalssl.ts
│   ├── filter-test-helper.ts
│   ├── history-api.ts
│   ├── history.js
│   ├── httpprovider.js
│   ├── metadata-e2e.ts
│   ├── metadata.js
│   ├── modules.js
│   ├── multiple-values.js
│   ├── nmea0183-filtering.ts
│   ├── notifications.ts
│   ├── oidc/
│   │   ├── authorization.test.ts
│   │   ├── config.test.ts
│   │   ├── crypto-service.test.ts
│   │   ├── discovery.test.ts
│   │   ├── id-token-validation.test.ts
│   │   ├── integration.test.ts
│   │   ├── oidc-auth.test.ts
│   │   ├── permission-mapping.test.ts
│   │   ├── pkce.test.ts
│   │   ├── settings-api.test.ts
│   │   ├── state.test.ts
│   │   ├── token-exchange.test.ts
│   │   ├── user-info.test.ts
│   │   ├── user-service.test.ts
│   │   └── userinfo-validation.test.ts
│   ├── plugin-crash-isolation.ts
│   ├── plugin-test-config/
│   │   └── package.json
│   ├── plugins.js
│   ├── providers.js
│   ├── put.js
│   ├── rate-limit.ts
│   ├── resources.ts
│   ├── scripts/
│   │   ├── mock-systemctl
│   │   └── signalk-server-setup
│   ├── seatalk1-filtering.ts
│   ├── security.js
│   ├── server-test-config/
│   │   ├── .npmrc
│   │   └── package.json
│   ├── servertestutilities.js
│   ├── sliding-session.ts
│   ├── ssl.ts
│   ├── staticData.js
│   ├── subscriptions.js
│   ├── ts-servertestutilities.ts
│   ├── unitpreferences.ts
│   ├── wasm-plugin-test-config/
│   │   └── package.json
│   ├── wasm-plugins.ts
│   ├── ws-connection-limit.ts
│   └── zones.ts
├── test-server-as-include/
│   ├── package.json
│   ├── run.sh
│   └── works-as-include.js
├── tools/
│   ├── README.md
│   ├── oidc-test-env/
│   │   ├── README.md
│   │   ├── authelia/
│   │   │   ├── configuration.yml
│   │   │   └── users_database.yml
│   │   ├── docker-compose.yml
│   │   └── traefik/
│   │       ├── dynamic.yml
│   │       └── traefik.yml
│   ├── test-auth-negative.sh
│   ├── test-oidc-all.sh
│   ├── test-oidc-flow.sh
│   └── test-oidc-sso.sh
├── tsconfig.base.json
├── tsconfig.json
├── typedoc.json
├── unitpreferences/
│   ├── README.md
│   ├── categories.json
│   ├── config.json
│   ├── custom-categories.json
│   ├── default-categories.json
│   ├── presets/
│   │   ├── imperial-uk.json
│   │   ├── imperial-us.json
│   │   ├── metric.json
│   │   ├── nautical-imperial-uk.json
│   │   ├── nautical-imperial-us.json
│   │   └── nautical-metric.json
│   ├── primary-categories.json
│   └── standard-units-definitions.json
└── util/
    └── start-stop.js
Download .txt
SYMBOL INDEX (2127 symbols across 295 files)

FILE: eslint.config.js
  function common (line 125) | function common(prefix = '') {

FILE: examples/wasm-plugins/example-anchor-watch-rust/src/lib.rs
  function sk_debug (line 20) | fn sk_debug(ptr: *const u8, len: usize);
  function sk_set_status (line 21) | fn sk_set_status(ptr: *const u8, len: usize);
  function sk_set_error (line 22) | fn sk_set_error(ptr: *const u8, len: usize);
  function sk_handle_message (line 23) | fn sk_handle_message(ptr: *const u8, len: usize);
  function sk_register_put_handler (line 24) | fn sk_register_put_handler(context_ptr: *const u8, context_len: usize, p...
  function debug (line 31) | fn debug(msg: &str) {
  function set_status (line 35) | fn set_status(msg: &str) {
  function set_error (line 39) | fn set_error(msg: &str) {
  function handle_message (line 43) | fn handle_message(msg: &str) {
  function register_put_handler (line 47) | fn register_put_handler(context: &str, path: &str) -> i32 {
  type PluginConfig (line 61) | struct PluginConfig {
  function default_max_radius (line 72) | fn default_max_radius() -> f64 { 50.0 }
  function default_interval (line 73) | fn default_interval() -> u32 { 10 }
  type PluginState (line 76) | struct PluginState {
  function allocate (line 90) | pub extern "C" fn allocate(size: usize) -> *mut u8 {
  function deallocate (line 99) | pub extern "C" fn deallocate(ptr: *mut u8, size: usize) {
  function plugin_id (line 149) | pub extern "C" fn plugin_id(out_ptr: *mut u8, out_max_len: usize) -> i32 {
  function plugin_name (line 155) | pub extern "C" fn plugin_name(out_ptr: *mut u8, out_max_len: usize) -> i...
  function plugin_schema (line 161) | pub extern "C" fn plugin_schema(out_ptr: *mut u8, out_max_len: usize) ->...
  function plugin_start (line 167) | pub extern "C" fn plugin_start(config_ptr: *const u8, config_len: usize)...
  function plugin_stop (line 215) | pub extern "C" fn plugin_stop() -> i32 {
  function handle_put_vessels_self_navigation_anchor_position (line 235) | pub extern "C" fn handle_put_vessels_self_navigation_anchor_position(
  function handle_put_vessels_self_navigation_anchor_maxRadius (line 283) | pub extern "C" fn handle_put_vessels_self_navigation_anchor_maxRadius(
  function handle_put_vessels_self_navigation_anchor_state (line 329) | pub extern "C" fn handle_put_vessels_self_navigation_anchor_state(
  function http_endpoints (line 355) | pub extern "C" fn http_endpoints(out_ptr: *mut u8, out_max_len: usize) -...
  function http_get_status (line 366) | pub extern "C" fn http_get_status(
  function http_get_position (line 392) | pub extern "C" fn http_get_position(
  function http_post_drop (line 415) | pub extern "C" fn http_post_drop(
  function emit_anchor_state (line 507) | fn emit_anchor_state(enabled: bool, lat: f64, lon: f64, radius: f64) {
  function write_string (line 527) | fn write_string(s: &str, ptr: *mut u8, max_len: usize) -> i32 {
  function haversine_distance (line 540) | fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {

FILE: examples/wasm-plugins/example-hello-assemblyscript/assembly/index.ts
  class HelloConfig (line 23) | class HelloConfig {
  class HelloPlugin (line 36) | class HelloPlugin extends Plugin {
    method logDebug (line 42) | private logDebug(message: string): void {
    method name (line 52) | name(): string {
    method schema (line 59) | schema(): string {
    method start (line 80) | start(configJson: string): i32 {
    method stop (line 144) | stop(): i32 {
    method emitWelcomeNotification (line 157) | private emitWelcomeNotification(): void {
    method emitTestDelta (line 179) | private emitTestDelta(): void {
    method emitHeartbeat (line 204) | emitHeartbeat(): void {
    method getUpdateInterval (line 231) | getUpdateInterval(): i32 {
  function plugin_name (line 243) | function plugin_name(): string {
  function plugin_schema (line 247) | function plugin_schema(): string {
  function plugin_start (line 251) | function plugin_start(configPtr: usize, configLen: usize): i32 {
  function plugin_stop (line 263) | function plugin_stop(): i32 {
  function poll (line 271) | function poll(): i32 {
  function http_endpoints (line 288) | function http_endpoints(): string {
  function handle_get_info (line 307) | function handle_get_info(requestPtr: usize, requestLen: usize): string {
  function handle_get_status (line 340) | function handle_get_status(

FILE: examples/wasm-plugins/example-routes-waypoints/assembly/index.ts
  class Waypoint (line 28) | class Waypoint {
    method toJSON (line 36) | toJSON(): string {
  class RoutePoint (line 68) | class RoutePoint {
  class Route (line 77) | class Route {
    method toJSON (line 84) | toJSON(): string {
  function findWaypointById (line 142) | function findWaypointById(id: string): Waypoint | null {
  function findRouteById (line 151) | function findRouteById(id: string): Route | null {
  function deleteWaypointById (line 160) | function deleteWaypointById(id: string): bool {
  function deleteRouteById (line 170) | function deleteRouteById(id: string): bool {
  function extractString (line 180) | function extractString(json: string, key: string): string {
  function extractNumber (line 192) | function extractNumber(json: string, key: string): f64 {
  function generateId (line 214) | function generateId(): string {
  function initializeSampleData (line 222) | function initializeSampleData(): void {
  class RoutesWaypointsPlugin (line 290) | class RoutesWaypointsPlugin extends Plugin {
    method name (line 293) | name(): string {
    method start (line 297) | start(configJson: string): i32 {
    method stop (line 328) | stop(): i32 {
    method schema (line 334) | schema(): string {
  function plugin_name (line 355) | function plugin_name(): string {
  function plugin_schema (line 359) | function plugin_schema(): string {
  function plugin_start (line 363) | function plugin_start(configPtr: usize, configLen: usize): i32 {
  function plugin_stop (line 373) | function plugin_stop(): i32 {
  function resources_list_resources (line 387) | function resources_list_resources(queryJson: string): string {
  function resources_get_resource (line 424) | function resources_get_resource(requestJson: string): string {
  function resources_set_resource (line 457) | function resources_set_resource(requestJson: string): string {
  function resources_delete_resource (line 565) | function resources_delete_resource(requestJson: string): string {

FILE: examples/wasm-plugins/example-weather-plugin/assembly/index.ts
  class WeatherConfig (line 30) | class WeatherConfig {
  class WeatherData (line 38) | class WeatherData {
    method toJSON (line 49) | toJSON(): string {
    method parse (line 72) | static parse(json: string): WeatherData | null {
  class WeatherPlugin (line 168) | class WeatherPlugin extends Plugin {
    method name (line 174) | name(): string {
    method start (line 178) | start(configJson: string): i32 {
    method stop (line 302) | stop(): i32 {
    method schema (line 308) | schema(): string {
    method emitTestWeatherData (line 340) | private emitTestWeatherData(): void {
    method fetchWeatherData (line 378) | private fetchWeatherData(): void {
  function plugin_name (line 466) | function plugin_name(): string {
  function plugin_schema (line 470) | function plugin_schema(): string {
  function plugin_start (line 474) | function plugin_start(configPtr: usize, configLen: usize): i32 {
  function plugin_stop (line 486) | function plugin_stop(): i32 {
  function resources_list_resources (line 501) | function resources_list_resources(queryJson: string): string {
  function resources_get_resource (line 522) | function resources_get_resource(requestJson: string): string {

FILE: examples/wasm-plugins/example-weather-provider/assembly/index.ts
  function registerWeatherProvider (line 43) | function registerWeatherProvider(providerName: string): bool {
  class WeatherData (line 58) | class WeatherData {
    method toJSON (line 82) | toJSON(): string {
  class WeatherWarning (line 122) | class WeatherWarning {
    method toJSON (line 129) | toJSON(): string {
  class WeatherConfig (line 152) | class WeatherConfig {
  function extractNumber (line 165) | function extractNumber(json: string, key: string): f64 {
  function extractString (line 185) | function extractString(json: string, key: string): string {
  function fetchCurrentWeather (line 199) | function fetchCurrentWeather(lat: f64, lon: f64): WeatherData | null {
  function fetchForecast (line 243) | function fetchForecast(
  class WeatherProviderPlugin (line 297) | class WeatherProviderPlugin extends Plugin {
    method name (line 300) | name(): string {
    method start (line 304) | start(configJson: string): i32 {
    method stop (line 394) | stop(): i32 {
    method schema (line 400) | schema(): string {
  function plugin_name (line 433) | function plugin_name(): string {
  function plugin_schema (line 437) | function plugin_schema(): string {
  function plugin_start (line 441) | function plugin_start(configPtr: usize, configLen: usize): i32 {
  function plugin_stop (line 451) | function plugin_stop(): i32 {
  class WeatherRequest (line 461) | class WeatherRequest {
    method parse (line 467) | static parse(json: string): WeatherRequest {
  function weather_get_observations (line 497) | function weather_get_observations(requestJson: string): string {
  function weather_get_forecasts (line 527) | function weather_get_forecasts(requestJson: string): string {
  function weather_get_warnings (line 564) | function weather_get_warnings(requestJson: string): string {

FILE: packages/assemblyscript-plugin-sdk/assembly/api.ts
  constant SK_VERSION_V1 (line 90) | const SK_VERSION_V1: i32 = 1
  constant SK_VERSION_V2 (line 91) | const SK_VERSION_V2: i32 = 2
  function emit (line 114) | function emit(delta: Delta, skVersion: i32 = SK_VERSION_V1): void {
  function setStatus (line 131) | function setStatus(message: string): void {
  function setError (line 147) | function setError(message: string): void {
  function debug (line 163) | function debug(message: string): void {
  function getSelfPath (line 184) | function getSelfPath(path: string): string | null {
  function getPath (line 222) | function getPath(path: string): string | null {
  function readConfig (line 252) | function readConfig(): string {
  function saveConfig (line 282) | function saveConfig(configJson: string): i32 {

FILE: packages/assemblyscript-plugin-sdk/assembly/network.ts
  function hasNetworkCapability (line 48) | function hasNetworkCapability(): boolean {

FILE: packages/assemblyscript-plugin-sdk/assembly/plugin.ts
  class PluginConfig (line 47) | class PluginConfig {

FILE: packages/assemblyscript-plugin-sdk/assembly/resources.ts
  function registerResourceProvider (line 57) | function registerResourceProvider(resourceType: string): bool {
  function hasResourceProviderCapability (line 78) | function hasResourceProviderCapability(): bool {
  class ResourceGetRequest (line 89) | class ResourceGetRequest {
    method parse (line 93) | static parse(jsonStr: string): ResourceGetRequest {
  class ResourceSetRequest (line 116) | class ResourceSetRequest {
    method parse (line 120) | static parse(jsonStr: string): ResourceSetRequest {
  class ResourceDeleteRequest (line 143) | class ResourceDeleteRequest {
    method parse (line 146) | static parse(jsonStr: string): ResourceDeleteRequest {

FILE: packages/assemblyscript-plugin-sdk/assembly/signalk.ts
  class Position (line 8) | class Position {
    method constructor (line 12) | constructor(latitude: f64, longitude: f64) {
    method toJSON (line 17) | toJSON(): string {
  class PathValue (line 25) | class PathValue {
    method constructor (line 29) | constructor(path: string, value: string) {
    method toJSON (line 34) | toJSON(): string {
  class Update (line 46) | class Update {
    method constructor (line 49) | constructor(values: PathValue[]) {
    method toJSON (line 53) | toJSON(): string {
  class Delta (line 68) | class Delta {
    method constructor (line 72) | constructor(context: string, updates: Update[]) {
    method toJSON (line 77) | toJSON(): string {
  type NotificationState (line 92) | enum NotificationState {
  type NotificationMethod (line 103) | enum NotificationMethod {
  class Notification (line 111) | class Notification {
    method constructor (line 116) | constructor(state: NotificationState, message: string) {
    method toJSON (line 122) | toJSON(): string {
  function createSimpleDelta (line 150) | function createSimpleDelta(path: string, value: string): Delta {

FILE: packages/assemblyscript-plugin-sdk/build/plugin.d.ts
  type NotificationState (line 4) | enum NotificationState {
  type NotificationMethod (line 17) | enum NotificationMethod {
  class __Internref4 (line 101) | class __Internref4 extends Number {

FILE: packages/assemblyscript-plugin-sdk/build/plugin.js
  function instantiate (line 1) | async function instantiate(module, imports = {}) {

FILE: packages/resources-provider-plugin/src/index.ts
  type ResourceProviderApp (line 13) | interface ResourceProviderApp extends ServerAPI, ResourceProviderRegistr...
  type ProviderSettings (line 15) | interface ProviderSettings {
  constant CONFIG_SCHEMA (line 26) | const CONFIG_SCHEMA = {
  constant CONFIG_UISCHEMA (line 80) | const CONFIG_UISCHEMA = {
  method registerWithRouter (line 125) | registerWithRouter(router) {

FILE: packages/resources-provider-plugin/src/types/store.ts
  type IResourceStore (line 3) | interface IResourceStore {
  type StoreRequestParams (line 15) | interface StoreRequestParams {

FILE: packages/server-admin-ui-dependencies/index.js
  method handleFailure (line 3) | handleFailure(result) {

FILE: packages/server-admin-ui/src/actions.ts
  function logoutAction (line 7) | async function logoutAction(): Promise<void> {
  function restartAction (line 23) | function restartAction(): void {
  function loginAction (line 34) | async function loginAction(
  function enableSecurity (line 63) | async function enableSecurity(
  function disableSecurity (line 91) | async function disableSecurity(
  function restoreSecurity (line 113) | async function restoreSecurity(
  function checkSecurityBackup (line 140) | async function checkSecurityBackup(): Promise<boolean> {
  function openServerEventsConnection (line 151) | function openServerEventsConnection(isReconnect?: boolean): void {
  function closeServerEventsConnection (line 155) | function closeServerEventsConnection(skipReconnect = false): void {
  function getWebSocketService (line 159) | function getWebSocketService() {

FILE: packages/server-admin-ui/src/components/Aside/Aside.tsx
  function Aside (line 1) | function Aside() {

FILE: packages/server-admin-ui/src/components/Footer/Footer.tsx
  function Footer (line 11) | function Footer() {

FILE: packages/server-admin-ui/src/components/Header/Header.tsx
  function Header (line 18) | function Header() {

FILE: packages/server-admin-ui/src/components/Sidebar/Sidebar.tsx
  type BadgeData (line 17) | interface BadgeData {
  type NavItemData (line 24) | interface NavItemData {
  type SidebarProps (line 42) | interface SidebarProps {
  function Sidebar (line 46) | function Sidebar({ location }: SidebarProps) {

FILE: packages/server-admin-ui/src/components/SidebarFooter/SidebarFooter.tsx
  function SidebarFooter (line 1) | function SidebarFooter() {

FILE: packages/server-admin-ui/src/components/SidebarForm/SidebarForm.tsx
  function SidebarForm (line 1) | function SidebarForm() {

FILE: packages/server-admin-ui/src/components/SidebarHeader/SidebarHeader.tsx
  function SidebarHeader (line 1) | function SidebarHeader() {

FILE: packages/server-admin-ui/src/components/SidebarMinimizer/SidebarMinimizer.tsx
  function SidebarMinimizer (line 3) | function SidebarMinimizer() {

FILE: packages/server-admin-ui/src/containers/Full/Full.tsx
  type ErrorBoundaryProps (line 34) | interface ErrorBoundaryProps {
  type ErrorBoundaryState (line 38) | interface ErrorBoundaryState {
  class ErrorBoundary (line 44) | class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryS...
    method constructor (line 45) | constructor(props: ErrorBoundaryProps) {
    method getDerivedStateFromError (line 50) | static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    method componentDidCatch (line 54) | override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): ...
    method render (line 58) | override render(): ReactNode {
  type ProtectedRouteProps (line 77) | interface ProtectedRouteProps {
  function loginRequired (line 82) | function loginRequired(
  function ProtectedRoute (line 96) | function ProtectedRoute({
  function Full (line 113) | function Full() {

FILE: packages/server-admin-ui/src/contexts/WebSocketContext.tsx
  type WebSocketProviderProps (line 10) | interface WebSocketProviderProps {
  function WebSocketProvider (line 18) | function WebSocketProvider({
  function useWebSocketContext (line 36) | function useWebSocketContext() {

FILE: packages/server-admin-ui/src/dataFetching.ts
  type Window (line 5) | interface Window {
  function fetchLoginStatus (line 20) | async function fetchLoginStatus(): Promise<void> {
  function fetchAllData (line 28) | async function fetchAllData(): Promise<void> {

FILE: packages/server-admin-ui/src/hooks/useWebSocket.ts
  type WebSocketState (line 8) | interface WebSocketState {
  function useWebSocket (line 15) | function useWebSocket(): WebSocketState {
  function useWebSocketStatus (line 30) | function useWebSocketStatus(): WebSocketStatus {
  function useSkSelf (line 39) | function useSkSelf(): string | null {
  function useDeltaMessages (line 48) | function useDeltaMessages(handler: DeltaMessageHandler): void {
  function useWebSocketActions (line 63) | function useWebSocketActions() {
  function getWebSocketService (line 77) | function getWebSocketService() {

FILE: packages/server-admin-ui/src/services/WebSocketService.ts
  type WebSocketStatus (line 4) | type WebSocketStatus =
  type DeltaMessageHandler (line 11) | type DeltaMessageHandler = (message: unknown) => void
  type StatusChangeHandler (line 12) | type StatusChangeHandler = (status: WebSocketStatus) => void
  type WebSocketServiceState (line 14) | interface WebSocketServiceState {
  type Listener (line 20) | type Listener = () => void
  type ZustandStateSetter (line 21) | type ZustandStateSetter = (
  class WebSocketService (line 27) | class WebSocketService {
    method setZustandState (line 43) | setZustandState(setState: ZustandStateSetter): void {
    method connect (line 47) | connect(isReconnect = false): void {
    method close (line 93) | close(skipReconnect = false): void {
    method reconnect (line 104) | reconnect(): void {
    method getWebSocket (line 109) | getWebSocket(): WebSocket | null {
    method getSkSelf (line 113) | getSkSelf(): string | null {
    method getStatus (line 117) | getStatus(): WebSocketStatus {
    method addDeltaHandler (line 137) | addDeltaHandler(handler: DeltaMessageHandler): () => void {
    method addStatusHandler (line 144) | addStatusHandler(handler: StatusChangeHandler): () => void {
    method handleMessage (line 151) | private handleMessage(message: unknown): void {
    method dispatchDelta (line 191) | private dispatchDelta(message: unknown): void {
    method handleServerEvent (line 201) | private handleServerEvent(msg: Record<string, unknown>): void {
    method updateState (line 273) | private updateState(updates: Partial<WebSocketServiceState>): void {
    method startReconnectTimer (line 306) | private startReconnectTimer(): void {
    method stopReconnectTimer (line 320) | private stopReconnectTimer(): void {

FILE: packages/server-admin-ui/src/store/index.ts
  type SignalKStore (line 27) | type SignalKStore = AppSlice &
  function useWsStatus (line 45) | function useWsStatus() {
  function useWsConnection (line 49) | function useWsConnection() {
  function useLoginStatus (line 60) | function useLoginStatus() {
  function useAppStore (line 64) | function useAppStore() {
  function useServerStats (line 68) | function useServerStats() {
  function useLogEntries (line 72) | function useLogEntries() {
  function useClearLogEntries (line 76) | function useClearLogEntries() {
  function usePathData (line 80) | function usePathData(context: string, path$SourceKey: string) {
  function useMetaData (line 84) | function useMetaData(context: string, path: string) {
  function useDataVersion (line 88) | function useDataVersion() {
  function useSourcePriorities (line 92) | function useSourcePriorities() {
  function useWebapps (line 96) | function useWebapps() {
  function useAddons (line 100) | function useAddons() {
  function usePlugins (line 104) | function usePlugins() {
  function useAccessRequests (line 108) | function useAccessRequests() {
  function useDevices (line 112) | function useDevices() {
  function useVesselInfo (line 116) | function useVesselInfo() {
  function useServerSpecification (line 120) | function useServerSpecification() {
  function useRestarting (line 124) | function useRestarting() {
  function useNodeInfo (line 128) | function useNodeInfo() {
  function useBackpressureWarning (line 132) | function useBackpressureWarning() {
  function useActivePreset (line 136) | function useActivePreset() {
  function useServerDefaultPreset (line 140) | function useServerDefaultPreset() {
  function usePresets (line 144) | function usePresets() {
  function usePresetDetails (line 148) | function usePresetDetails() {
  function useUnitDefinitions (line 152) | function useUnitDefinitions() {
  function useDefaultCategories (line 156) | function useDefaultCategories() {
  function useUnitPrefsLoaded (line 160) | function useUnitPrefsLoaded() {
  function useUnitCategories (line 164) | function useUnitCategories() {

FILE: packages/server-admin-ui/src/store/slices/appSlice.ts
  function nameCollator (line 27) | function nameCollator<T extends { name: string }>(left: T, right: T): nu...
  type AppSliceState (line 37) | interface AppSliceState {
  type AppSliceActions (line 57) | interface AppSliceActions {
  type AppSlice (line 88) | type AppSlice = AppSliceState & AppSliceActions

FILE: packages/server-admin-ui/src/store/slices/dataSlice.ts
  type PathData (line 3) | interface PathData {
  type RendererConfig (line 13) | interface RendererConfig {
  type MetaData (line 19) | interface MetaData {
  type DataSliceState (line 26) | interface DataSliceState {
  type DataSliceActions (line 35) | interface DataSliceActions {
  type DataSlice (line 53) | type DataSlice = DataSliceState & DataSliceActions

FILE: packages/server-admin-ui/src/store/slices/prioritiesSlice.ts
  function checkTimeouts (line 9) | function checkTimeouts(priorities: SourcePriority[]): boolean {
  type PrioritiesSliceState (line 31) | interface PrioritiesSliceState {
  type PrioritiesSliceActions (line 35) | interface PrioritiesSliceActions {
  type PrioritiesSlice (line 53) | type PrioritiesSlice = PrioritiesSliceState & PrioritiesSliceActions

FILE: packages/server-admin-ui/src/store/slices/unitPreferencesSlice.ts
  type DefaultCategories (line 9) | interface DefaultCategories {
  type UnitPreferencesSliceState (line 17) | interface UnitPreferencesSliceState {
  type UnitPreferencesSliceActions (line 28) | interface UnitPreferencesSliceActions {
  type UnitPreferencesSlice (line 40) | type UnitPreferencesSlice = UnitPreferencesSliceState &
  constant DEFAULT_PRESETS (line 43) | const DEFAULT_PRESETS: PresetInfo[] = [
  function fetchPresetsFromServer (line 49) | async function fetchPresetsFromServer(): Promise<PresetInfo[]> {
  function fetchActivePresetFromServer (line 85) | async function fetchActivePresetFromServer(): Promise<string> {
  function fetchServerDefaultPresetFromServer (line 113) | async function fetchServerDefaultPresetFromServer(): Promise<string> {

FILE: packages/server-admin-ui/src/store/slices/wsSlice.ts
  type WebSocketStatus (line 3) | type WebSocketStatus =
  type DeltaMessageHandler (line 10) | type DeltaMessageHandler = (message: unknown) => void
  type WsSliceState (line 12) | interface WsSliceState {
  type WsSliceActions (line 18) | interface WsSliceActions {
  type WsSlice (line 29) | type WsSlice = WsSliceState & WsSliceActions
  function stopReconnectTimer (line 44) | function stopReconnectTimer(): void {
  function startReconnectTimer (line 51) | function startReconnectTimer(): void {

FILE: packages/server-admin-ui/src/store/types.ts
  type LogEntry (line 1) | interface LogEntry {
  type LogState (line 6) | interface LogState {
  type AppStoreState (line 12) | interface AppStoreState {
  type AppInfo (line 23) | interface AppInfo {
  type InstallingApp (line 31) | interface InstallingApp {
  type LoginStatus (line 40) | interface LoginStatus {
  type ServerSpecification (line 54) | interface ServerSpecification {
  type ProviderStatus (line 63) | interface ProviderStatus {
  type AccessRequest (line 69) | interface AccessRequest {
  type DeviceInfo (line 75) | interface DeviceInfo {
  type DiscoveredProvider (line 83) | interface DiscoveredProvider {
  type RestoreStatus (line 88) | interface RestoreStatus {
  type VesselInfo (line 94) | interface VesselInfo {
  type NodeInfo (line 101) | interface NodeInfo {
  type SourcePriority (line 107) | interface SourcePriority {
  type PathPriority (line 112) | interface PathPriority {
  type SaveState (line 117) | interface SaveState {
  type SourcePrioritiesData (line 124) | interface SourcePrioritiesData {
  type BackpressureWarning (line 129) | interface BackpressureWarning {
  type ProviderStatistics (line 135) | interface ProviderStatistics {
  type ServerStatistics (line 142) | interface ServerStatistics {
  type Plugin (line 151) | interface Plugin {
  type Webapp (line 159) | interface Webapp {
  type Addon (line 165) | interface Addon {
  type PresetInfo (line 170) | interface PresetInfo {
  type PresetCategoryConfig (line 177) | interface PresetCategoryConfig {
  type PresetDetails (line 182) | interface PresetDetails {
  type UnitConversion (line 188) | interface UnitConversion {
  type UnitDefinition (line 193) | interface UnitDefinition {
  type UnitDefinitions (line 197) | type UnitDefinitions = Record<string, UnitDefinition>
  type DefaultCategory (line 199) | interface DefaultCategory {
  type CategoryInfo (line 204) | interface CategoryInfo {

FILE: packages/server-admin-ui/src/test/setup.ts
  method constructor (line 21) | constructor(url: string) {
  method send (line 25) | send(): void {
  method close (line 29) | close(): void {
  method observe (line 54) | observe(): void {
  method unobserve (line 57) | unobserve(): void {
  method disconnect (line 60) | disconnect(): void {

FILE: packages/server-admin-ui/src/utils/unitConversion.ts
  function getCompiledFormula (line 5) | function getCompiledFormula(formula: string): EvalFunction {
  type ConvertedValue (line 14) | interface ConvertedValue {
  type PresetDetails (line 19) | interface PresetDetails {
  type UnitConversion (line 31) | interface UnitConversion {
  type UnitDefinition (line 38) | interface UnitDefinition {
  type UnitDefinitions (line 42) | type UnitDefinitions = Record<string, UnitDefinition>
  function convertValue (line 44) | function convertValue(
  function convertFromSI (line 92) | function convertFromSI(
  function convertToSI (line 108) | function convertToSI(
  type AvailableUnit (line 125) | interface AvailableUnit {
  function getAvailableUnits (line 130) | function getAvailableUnits(

FILE: packages/server-admin-ui/src/views/Configuration/Configuration.tsx
  type PluginSchema (line 25) | interface PluginSchema {
  type PluginData (line 30) | interface PluginData {
  type Plugin (line 38) | interface Plugin {
  function PluginConfigurationList (line 59) | function PluginConfigurationList() {
  type PluginConfigCardProps (line 473) | interface PluginConfigCardProps {
  function PluginConfigCard (line 479) | function PluginConfigCard({

FILE: packages/server-admin-ui/src/views/Configuration/EmbeddedPluginConfigurationForm.tsx
  type PluginErrorBoundaryProps (line 14) | interface PluginErrorBoundaryProps {
  type PluginErrorBoundaryState (line 19) | interface PluginErrorBoundaryState {
  class PluginErrorBoundary (line 24) | class PluginErrorBoundary extends Component<
    method getDerivedStateFromError (line 30) | static getDerivedStateFromError(error: Error): PluginErrorBoundaryState {
    method render (line 34) | override render() {
  type PluginData (line 57) | interface PluginData {
  type EmbeddedPluginConfigurationFormProps (line 66) | interface EmbeddedPluginConfigurationFormProps {
  type ConfigPanelProps (line 71) | interface ConfigPanelProps {
  function EmbeddedPluginConfigurationForm (line 76) | function EmbeddedPluginConfigurationForm({

FILE: packages/server-admin-ui/src/views/Dashboard/Dashboard.tsx
  type ProviderStatusItem (line 12) | interface ProviderStatusItem {
  function Dashboard (line 21) | function Dashboard() {
  function pluginNameLink (line 301) | function pluginNameLink(id: string): ReactNode {
  function providerIdLink (line 305) | function providerIdLink(id: string): ReactNode {

FILE: packages/server-admin-ui/src/views/DataBrowser/CopyToClipboardWithFade.tsx
  function copyToClipboard (line 3) | function copyToClipboard(text: string): Promise<void> {
  type CopyToClipboardWithFadeProps (line 29) | interface CopyToClipboardWithFadeProps {
  function CopyToClipboardWithFade (line 34) | function CopyToClipboardWithFade({

FILE: packages/server-admin-ui/src/views/DataBrowser/DataBrowser.tsx
  constant TIMESTAMP_FORMAT (line 36) | const TIMESTAMP_FORMAT = 'MM/DD HH:mm:ss'
  constant TIME_ONLY_FORMAT (line 37) | const TIME_ONLY_FORMAT = 'HH:mm:ss'
  function matchesSearch (line 47) | function matchesSearch(key: string, search: string): boolean {
  type DeltaMessage (line 58) | interface DeltaMessage {
  type SelectOption (line 78) | interface SelectOption {
  type SourceDevice (line 95) | interface SourceDevice {
  type Sources (line 104) | interface Sources {

FILE: packages/server-admin-ui/src/views/DataBrowser/DataRow.tsx
  type DataRowProps (line 15) | interface DataRowProps {
  type ValueRendererProps (line 26) | interface ValueRendererProps {
  function findCategoryForPath (line 38) | function findCategoryForPath(
  function DataRow (line 59) | function DataRow({
  function ValueRenderer (line 224) | function ValueRenderer({

FILE: packages/server-admin-ui/src/views/DataBrowser/GranularSubscriptionManager.ts
  constant STATE (line 7) | const STATE = {
  type SubscriptionState (line 13) | type SubscriptionState = (typeof STATE)[keyof typeof STATE]
  constant DEBUG (line 16) | const DEBUG = false
  type SubscriptionMessage (line 20) | interface SubscriptionMessage {
  type MessageHandler (line 27) | type MessageHandler = (msg: unknown) => void
  type WebSocketLike (line 29) | interface WebSocketLike {
  class GranularSubscriptionManager (line 34) | class GranularSubscriptionManager {
    method setWebSocket (line 47) | setWebSocket(ws: WebSocketLike | null): void {
    method setMessageHandler (line 51) | setMessageHandler(handler: MessageHandler): void {
    method startDiscovery (line 59) | startDiscovery(): void {
    method requestPaths (line 87) | requestPaths(visiblePathKeys: string[], allPathKeys: string[]): void {
    method _expandWithOverscan (line 129) | private _expandWithOverscan(
    method _pathsAreSimilar (line 159) | private _pathsAreSimilar(
    method _executeResubscription (line 179) | private _executeResubscription(newPaths: Set<string>): void {
    method _extractUniquePaths (line 243) | private _extractUniquePaths(path$SourceKeys: Set<string>): string[] {
    method handleMessage (line 259) | handleMessage(msg: unknown): void {
    method unsubscribeAll (line 268) | unsubscribeAll(): void {
    method cancelPending (line 293) | cancelPending(): void {
    method getState (line 304) | getState(): {
    method _send (line 318) | private _send(msg: SubscriptionMessage): void {

FILE: packages/server-admin-ui/src/views/DataBrowser/Meta.tsx
  type DisplayScaleValue (line 34) | interface DisplayScaleValue {
  type DisplayUnits (line 41) | interface DisplayUnits {
  type MetaData (line 49) | interface MetaData {
  type Zone (line 68) | interface Zone {
  function generateZoneId (line 77) | function generateZoneId(): string {
  type MetaProps (line 81) | interface MetaProps {
  type MetaFormRowProps (line 88) | interface MetaFormRowProps {
  type ValueRenderProps (line 103) | interface ValueRenderProps {
  constant UNITS (line 110) | const UNITS: Record<string, string> = {
  constant METAFIELDS (line 134) | const METAFIELDS = [
  constant DISPLAYTYPES (line 152) | const DISPLAYTYPES = ['linear', 'logarithmic', 'squareroot', 'power']
  constant STATES (line 154) | const STATES = ['nominal', 'alert', 'warn', 'alarm', 'emergency']
  constant STATE_COLORS (line 156) | const STATE_COLORS: Record<string, string> = {
  constant DEFAULT_CATEGORIES (line 164) | const DEFAULT_CATEGORIES = [
  constant CATEGORY_BADGE_COLORS (line 192) | const CATEGORY_BADGE_COLORS: Record<string, string> = {
  type CategorySelectProps (line 205) | interface CategorySelectProps extends ValueRenderProps {
  constant METAFIELDRENDERERS (line 533) | const METAFIELDRENDERERS: Record<
  type UnknownMetaFormRowProps (line 954) | interface UnknownMetaFormRowProps {
  type ZoneProps (line 989) | interface ZoneProps {
  type ZonesProps (line 1177) | interface ZonesProps {
  function Zones (line 1188) | function Zones({

FILE: packages/server-admin-ui/src/views/DataBrowser/TimestampCell.tsx
  type TimestampCellProps (line 1) | interface TimestampCellProps {
  function TimestampCell (line 11) | function TimestampCell({ timestamp, isPaused, className }: TimestampCell...

FILE: packages/server-admin-ui/src/views/DataBrowser/ValueRenderers.tsx
  type RendererProps (line 13) | interface RendererProps {
  type HTMLRendererProps (line 21) | interface HTMLRendererProps {
  type DirectionRendererProps (line 26) | interface DirectionRendererProps {
  type AttitudeValue (line 31) | interface AttitudeValue {
  type AttitudeRendererProps (line 36) | interface AttitudeRendererProps {
  type NotificationValue (line 41) | interface NotificationValue {
  type NotificationRendererProps (line 47) | interface NotificationRendererProps {
  type LargeArrayRendererProps (line 51) | interface LargeArrayRendererProps {
  type MeterRendererProps (line 55) | interface MeterRendererProps {
  type PositionValue (line 66) | interface PositionValue {
  type PositionRendererProps (line 71) | interface PositionRendererProps {
  type Satellite (line 75) | interface Satellite {
  type SatellitesInViewValue (line 82) | interface SatellitesInViewValue {
  type SatellitesInViewRendererProps (line 87) | interface SatellitesInViewRendererProps {
  function radiansToDegrees (line 91) | function radiansToDegrees(radians: number): number {
  type RendererComponent (line 582) | type RendererComponent = ComponentType<RendererProps>
  function createLazySuspenseWrapper (line 584) | function createLazySuspenseWrapper(
  constant VALUE_RENDERERS (line 609) | const VALUE_RENDERERS: Record<string, RendererComponent> = {

FILE: packages/server-admin-ui/src/views/DataBrowser/VirtualizedDataTable.tsx
  type VisibleItem (line 13) | interface VisibleItem {
  type VirtualizedDataTableProps (line 18) | interface VirtualizedDataTableProps {
  function VirtualizedDataTable (line 30) | function VirtualizedDataTable({

FILE: packages/server-admin-ui/src/views/DataBrowser/VirtualizedMetaTable.tsx
  type MetaRowProps (line 6) | interface MetaRowProps {
  type VirtualizedMetaTableProps (line 36) | interface VirtualizedMetaTableProps {
  function VirtualizedMetaTable (line 44) | function VirtualizedMetaTable({

FILE: packages/server-admin-ui/src/views/DataBrowser/pathUtils.ts
  function getPath$SourceKey (line 4) | function getPath$SourceKey(path: string, source?: string): string {
  function getPathFromKey (line 8) | function getPathFromKey(path$SourceKey: string): string {

FILE: packages/server-admin-ui/src/views/DataBrowser/usePathData.ts
  constant THROTTLE_MS (line 5) | const THROTTLE_MS = 200 // max 5 UI re-renders per second per path
  function usePathData (line 7) | function usePathData(
  function useMetaData (line 71) | function useMetaData(

FILE: packages/server-admin-ui/src/views/Playground.tsx
  constant DELTAS_TAB_ID (line 19) | const DELTAS_TAB_ID = 'deltas'
  constant PATHS_TAB_ID (line 20) | const PATHS_TAB_ID = 'paths'
  constant N2KJSON_TAB_ID (line 21) | const N2KJSON_TAB_ID = 'n2kjson'
  constant PUTRESULTS_TAB_ID (line 22) | const PUTRESULTS_TAB_ID = 'putresults'
  constant LINT_ERROR_TAB_ID (line 23) | const LINT_ERROR_TAB_ID = 'lintErrors'
  type PathData (line 25) | interface PathData {
  type Delta (line 32) | interface Delta {
  type SendResponse (line 43) | interface SendResponse {
  function isJson (line 51) | function isJson(input: string): boolean {
  function N2kJsonPanel (line 60) | function N2kJsonPanel({ n2kData }: { n2kData: unknown[] }) {

FILE: packages/server-admin-ui/src/views/ServerConfig/BackupRestore.tsx
  constant RESTORE_NONE (line 14) | const RESTORE_NONE = 0
  constant RESTORE_VALIDATING (line 15) | const RESTORE_VALIDATING = 1
  constant RESTORE_CONFIRM (line 16) | const RESTORE_CONFIRM = 2
  constant RESTORE_RUNNING (line 17) | const RESTORE_RUNNING = 3
  type RestoreStatus (line 19) | interface RestoreStatus {

FILE: packages/server-admin-ui/src/views/ServerConfig/BasicProvider.tsx
  type ProviderOptions (line 15) | interface ProviderOptions {
  type ProviderValue (line 52) | interface ProviderValue {
  type DeviceListMap (line 62) | interface DeviceListMap {
  type OnChangeHandler (line 70) | type OnChangeHandler = (
  type OnPropChangeHandler (line 76) | type OnPropChangeHandler = (
  type BasicProviderProps (line 82) | interface BasicProviderProps {
  type TextInputProps (line 88) | interface TextInputProps {
  type TextAreaInputProps (line 96) | interface TextAreaInputProps {
  type DeviceInputProps (line 105) | interface DeviceInputProps {
  type LoggingInputProps (line 110) | interface LoggingInputProps {
  type ValidateChecksumInputProps (line 115) | interface ValidateChecksumInputProps {
  type OverrideTimestampsProps (line 120) | interface OverrideTimestampsProps {
  type TypeComponentProps (line 125) | interface TypeComponentProps {
  constant TYPE_COMPONENTS (line 132) | const TYPE_COMPONENTS: Record<
  function BasicProvider (line 143) | function BasicProvider({
  function TextInput (line 250) | function TextInput({ name, title, value, helpText, onChange }: TextInput...
  function TextAreaInput (line 269) | function TextAreaInput({
  type TestConnectionResult (line 296) | interface TestConnectionResult {
  type AccessRequestState (line 305) | interface AccessRequestState {
  function TokenInput (line 311) | function TokenInput({
  function DeviceInput (line 579) | function DeviceInput({ value, onChange }: DeviceInputProps) {
  function LoggingInput (line 666) | function LoggingInput({ value, onChange }: LoggingInputProps) {
  function ValidateChecksumInput (line 695) | function ValidateChecksumInput({
  function OverrideTimestamps (line 744) | function OverrideTimestamps({ value, onChange }: OverrideTimestampsProps) {
  function RemoveNullsInput (line 770) | function RemoveNullsInput({
  function AppendChecksum (line 802) | function AppendChecksum({
  function SentenceEventInput (line 845) | function SentenceEventInput({
  function DataTypeInput (line 863) | function DataTypeInput({
  function BaudRateInput (line 905) | function BaudRateInput({
  function BaudRateInputCanboat (line 923) | function BaudRateInputCanboat({
  function StdOutInput (line 944) | function StdOutInput({
  function IgnoredSentences (line 978) | function IgnoredSentences({
  function PortInput (line 1014) | function PortInput({
  function HostInput (line 1032) | function HostInput({
  function NoDataReceivedTimeoutInput (line 1050) | function NoDataReceivedTimeoutInput({
  function RemoteSelfInput (line 1068) | function RemoteSelfInput({
  function Suppress0183Checkbox (line 1086) | function Suppress0183Checkbox({
  function UseCanNameInput (line 1123) | function UseCanNameInput({
  function CreateDeviceInput (line 1155) | function CreateDeviceInput({
  function CamelCaseCompatInput (line 1193) | function CamelCaseCompatInput({
  function CollectNetworkStatsInput (line 1227) | function CollectNetworkStatsInput({
  function NMEA2000 (line 1259) | function NMEA2000({ value, onChange, hasAnalyzer }: TypeComponentProps) {
  function NMEA0183 (line 1391) | function NMEA0183({ value, onChange }: TypeComponentProps) {
  function SignalK (line 1457) | function SignalK({ value, onChange }: TypeComponentProps) {
  function Seatalk (line 1576) | function Seatalk({ value, onChange }: TypeComponentProps) {
  function FileStream (line 1640) | function FileStream({ value, onChange, hasAnalyzer }: TypeComponentProps) {

FILE: packages/server-admin-ui/src/views/ServerConfig/N2KFilters.tsx
  type N2KFilter (line 10) | interface N2KFilter {
  type ProviderOptions (line 15) | interface ProviderOptions {
  type ProviderValue (line 22) | interface ProviderValue {
  type N2KFiltersProps (line 27) | interface N2KFiltersProps {
  function N2KFilters (line 36) | function N2KFilters({ value, onChange }: N2KFiltersProps) {

FILE: packages/server-admin-ui/src/views/ServerConfig/PluginConfigurationForm.tsx
  constant GRID_COLUMNS (line 19) | const GRID_COLUMNS = {
  constant CSS_CLASSES (line 25) | const CSS_CLASSES = {
  type ButtonProps (line 47) | interface ButtonProps {
  type ArrayFieldItemTemplateProps (line 75) | interface ArrayFieldItemTemplateProps {
  type FieldTemplateProps (line 170) | interface FieldTemplateProps {
  type ObjectFieldTemplateProps (line 222) | interface ObjectFieldTemplateProps {
  type ArrayFieldTemplateProps (line 251) | interface ArrayFieldTemplateProps {
  type PluginData (line 409) | interface PluginData {
  type PluginSchema (line 417) | interface PluginSchema {
  type Plugin (line 422) | interface Plugin {
  type PluginConfigurationFormProps (line 429) | interface PluginConfigurationFormProps {
  function PluginConfigurationForm (line 434) | function PluginConfigurationForm({

FILE: packages/server-admin-ui/src/views/ServerConfig/ProvidersConfiguration.tsx
  type Provider (line 17) | interface Provider {
  type ProvidersData (line 46) | interface ProvidersData {
  type ApplicableStatusProps (line 408) | interface ApplicableStatusProps {
  type ProviderTypeProps (line 418) | interface ProviderTypeProps {

FILE: packages/server-admin-ui/src/views/ServerConfig/ServerLog.tsx
  type LogEntry (line 21) | interface LogEntry {
  type LogState (line 26) | interface LogState {
  type SelectOption (line 32) | interface SelectOption {
  function ServerLogs (line 37) | function ServerLogs() {
  type LogListProps (line 223) | interface LogListProps {
  function LogList (line 227) | function LogList({ value }: LogListProps) {
  type LogRowProps (line 261) | interface LogRowProps {
  function LogRow (line 265) | function LogRow({ log }: LogRowProps) {

FILE: packages/server-admin-ui/src/views/ServerConfig/ServerUpdate.tsx
  type InstallingApp (line 7) | interface InstallingApp {
  type AppStore (line 13) | interface AppStore {

FILE: packages/server-admin-ui/src/views/ServerConfig/Settings.tsx
  type ServerSettingsData (line 16) | interface ServerSettingsData {

FILE: packages/server-admin-ui/src/views/ServerConfig/SourcePriorities.tsx
  type Priority (line 19) | interface Priority {
  type PathPriority (line 24) | interface PathPriority {
  type SelectOption (line 29) | interface SelectOption {
  function fetchSourceRefs (line 34) | function fetchSourceRefs(path: string, cb: (refs: string[]) => void) {
  type PrefsEditorProps (line 49) | interface PrefsEditorProps {
  function fetchAvailablePaths (line 182) | function fetchAvailablePaths(cb: (paths: string[]) => void) {

FILE: packages/server-admin-ui/src/views/ServerConfig/UnitPreferencesSettings.tsx
  type UploadStatus (line 21) | type UploadStatus = 'uploading' | 'success' | 'duplicate' | 'error' | null

FILE: packages/server-admin-ui/src/views/ServerConfig/VesselConfiguration.tsx
  type VesselData (line 11) | interface VesselData {

FILE: packages/server-admin-ui/src/views/Webapps/Embedded.tsx
  type WebappErrorBoundaryState (line 20) | interface WebappErrorBoundaryState {
  type WebappErrorBoundaryProps (line 25) | interface WebappErrorBoundaryProps {
  class WebappErrorBoundary (line 30) | class WebappErrorBoundary extends Component<
    method getDerivedStateFromError (line 36) | static getDerivedStateFromError(error: Error): WebappErrorBoundaryState {
    method render (line 44) | override render() {
  type WebSocketParams (line 98) | interface WebSocketParams {
  type AdminUI (line 104) | interface AdminUI {
  type EmbeddedComponentProps (line 120) | interface EmbeddedComponentProps {
  function Embedded (line 125) | function Embedded() {

FILE: packages/server-admin-ui/src/views/Webapps/EmbeddedAsyncApi.tsx
  type AsyncApiSpec (line 3) | interface AsyncApiSpec {
  type AsyncApiDoc (line 8) | interface AsyncApiDoc {
  function schemaToString (line 42) | function schemaToString(schema: Record<string, unknown>, indent = 0): st...
  function EmbeddedAsyncApi (line 71) | function EmbeddedAsyncApi() {

FILE: packages/server-admin-ui/src/views/Webapps/EmbeddedDocs.tsx
  function EmbeddedDocs (line 4) | function EmbeddedDocs() {

FILE: packages/server-admin-ui/src/views/Webapps/Webapp.tsx
  type SignalKInfo (line 8) | interface SignalKInfo {
  type WebAppInfo (line 13) | interface WebAppInfo {
  type WebappProps (line 20) | interface WebappProps {
  function urlToWebapp (line 25) | function urlToWebapp(webAppInfo: WebAppInfo): string {
  function Webapp (line 31) | function Webapp({ webAppInfo, ...attributes }: WebappProps) {

FILE: packages/server-admin-ui/src/views/Webapps/Webapps.tsx
  type WebAppInfo (line 8) | interface WebAppInfo {
  type AddonModule (line 18) | interface AddonModule {
  type AddonPanelProps (line 22) | interface AddonPanelProps {
  function Webapps (line 27) | function Webapps() {

FILE: packages/server-admin-ui/src/views/Webapps/dynamicutilities.ts
  type ShareScopeEntry (line 4) | interface ShareScopeEntry {
  type ShareScope (line 15) | interface ShareScope {
  type FederationInstance (line 21) | interface FederationInstance {
  type Container (line 27) | interface Container {
  type Window (line 33) | interface Window {
  constant APP_PANEL (line 592) | const APP_PANEL = './AppPanel'
  constant ADDON_PANEL (line 593) | const ADDON_PANEL = './AddonPanel'
  constant PLUGIN_CONFIG_PANEL (line 594) | const PLUGIN_CONFIG_PANEL = './PluginConfigurationPanel'

FILE: packages/server-admin-ui/src/views/Webapps/loadingerror.tsx
  type LoadingErrorProps (line 1) | interface LoadingErrorProps {
  function LoadingError (line 5) | function LoadingError({ message }: LoadingErrorProps) {

FILE: packages/server-admin-ui/src/views/appstore/Apps/Apps.tsx
  type InstallingApp (line 13) | interface InstallingApp {
  type AppInfo (line 19) | interface AppInfo {
  type AppStore (line 33) | interface AppStore {

FILE: packages/server-admin-ui/src/views/appstore/Apps/WarningBox.tsx
  type WarningBoxProps (line 5) | interface WarningBoxProps {
  function WarningBox (line 9) | function WarningBox({ children }: WarningBoxProps) {

FILE: packages/server-admin-ui/src/views/appstore/AppsList.tsx
  type AppData (line 6) | interface AppData {
  function AppListItem (line 18) | function AppListItem(app: AppData) {
  type AppListProps (line 58) | interface AppListProps {
  function AppList (line 62) | function AppList({ apps: propsApps }: AppListProps) {

FILE: packages/server-admin-ui/src/views/appstore/Grid/cell-renderers/ActionCellRenderer.tsx
  type PluginDataSize (line 19) | interface PluginDataSize {
  function formatBytes (line 25) | function formatBytes(bytes: number): string {
  type AppData (line 33) | interface AppData {
  type ActionCellRendererProps (line 51) | interface ActionCellRendererProps {
  function ActionCellRenderer (line 55) | function ActionCellRenderer({

FILE: packages/server-admin-ui/src/views/security/AccessRequests.tsx
  type AccessRequestData (line 23) | interface AccessRequestData {
  type ProcessingState (line 33) | interface ProcessingState {
  function AccessRequests (line 38) | function AccessRequests() {

FILE: packages/server-admin-ui/src/views/security/Devices.tsx
  type PermissionType (line 23) | type PermissionType = 'readonly' | 'readwrite' | 'admin'
  type Device (line 25) | interface Device {
  function convertPermissions (line 33) | function convertPermissions(type: PermissionType | undefined): string {
  function isExpired (line 44) | function isExpired(device: Device): boolean {
  function formatExpiry (line 48) | function formatExpiry(device: Device): string {
  function Devices (line 70) | function Devices() {

FILE: packages/server-admin-ui/src/views/security/EnableSecurity.tsx
  type EnableSecurityState (line 30) | interface EnableSecurityState {
  function EnableSecurity (line 34) | function EnableSecurity() {

FILE: packages/server-admin-ui/src/views/security/Login.tsx
  type LoginState (line 39) | interface LoginState {
  function Login (line 43) | function Login() {

FILE: packages/server-admin-ui/src/views/security/OIDCSettings.tsx
  type TestResult (line 19) | interface TestResult {
  type SaveResult (line 28) | interface SaveResult {
  type EnvOverrides (line 33) | interface EnvOverrides {
  type OIDCConfig (line 37) | interface OIDCConfig {

FILE: packages/server-admin-ui/src/views/security/Register.tsx
  type FormFields (line 13) | interface FormFields {
  type RegisterState (line 19) | interface RegisterState {
  function Register (line 24) | function Register() {

FILE: packages/server-admin-ui/src/views/security/Settings.tsx
  type SecurityConfig (line 23) | interface SecurityConfig {
  function Settings (line 32) | function Settings() {
  function DisableSecurity (line 273) | function DisableSecurity() {

FILE: packages/server-admin-ui/src/views/security/Users.tsx
  type UserType (line 25) | type UserType = 'readonly' | 'readwrite' | 'admin'
  type User (line 27) | interface User {
  function convertType (line 37) | function convertType(type: UserType | undefined): string {
  function Users (line 48) | function Users() {

FILE: packages/server-admin-ui/vite.config.ts
  function replaceAddonScripts (line 9) | function replaceAddonScripts() {
  function stripSvgFonts (line 28) | function stripSvgFonts() {

FILE: packages/server-api/src/autopilotapi.ts
  type AutopilotUpdateAttrib (line 7) | type AutopilotUpdateAttrib =
  constant AUTOPILOTUPDATEATTRIBS (line 19) | const AUTOPILOTUPDATEATTRIBS: AutopilotUpdateAttrib[] = [
  type AutopilotAlarm (line 40) | type AutopilotAlarm =
  constant AUTOPILOTALARMS (line 51) | const AUTOPILOTALARMS: AutopilotAlarm[] = [
  type TackGybeDirection (line 71) | type TackGybeDirection = 'port' | 'starboard'
  type AutopilotApi (line 99) | interface AutopilotApi {
  type AutopilotProvider (line 127) | interface AutopilotProvider {
  type AutopilotStateDef (line 439) | interface AutopilotStateDef {
  type AutopilotActionDef (line 445) | interface AutopilotActionDef {
  type AutopilotOptions (line 452) | interface AutopilotOptions {
  type AutopilotInfo (line 459) | interface AutopilotInfo {
  type AutopilotProviderRegistry (line 470) | interface AutopilotProviderRegistry {

FILE: packages/server-api/src/brand.ts
  type Brand (line 21) | type Brand<Type, Name> = Type & { [__brand]: Name }

FILE: packages/server-api/src/course.ts
  type CourseApi (line 7) | interface CourseApi {

FILE: packages/server-api/src/coursetypes.ts
  type HrefDestination (line 5) | interface HrefDestination {
  type PositionDestination (line 10) | interface PositionDestination {
  type PointDestination (line 15) | type PointDestination = HrefDestination | PositionDestination
  type RouteDestination (line 18) | interface RouteDestination {
  type ActiveRoute (line 26) | interface ActiveRoute {
  type NextPreviousPoint (line 35) | interface NextPreviousPoint {
  type CoursePointType (line 42) | type CoursePointType = Brand<string, 'coursepointtype'>
  constant COURSE_POINT_TYPES (line 45) | const COURSE_POINT_TYPES = {
  type CourseInfo (line 52) | interface CourseInfo {

FILE: packages/server-api/src/deltas.ts
  type WithContext (line 5) | interface WithContext {
  type NormalizedBaseDelta (line 10) | type NormalizedBaseDelta = {
  type NormalizedMetaDelta (line 20) | type NormalizedMetaDelta = NormalizedBaseDelta & {
  type NormalizedValueDelta (line 26) | type NormalizedValueDelta = NormalizedBaseDelta & {
  type NormalizedDelta (line 32) | type NormalizedDelta = NormalizedValueDelta | NormalizedMetaDelta
  type SourceRef (line 35) | type SourceRef = Brand<string, 'sourceRef'>
  type Source (line 38) | type Source = any
  type Path (line 41) | type Path = Brand<string, 'path'>
  type Timestamp (line 43) | type Timestamp = Brand<string, 'timestamp'>
  type Context (line 45) | type Context = Brand<string, 'context'>
  type NotificationId (line 47) | type NotificationId = Brand<string, 'notificationId'>
  type Value (line 50) | type Value = object | number | string | null | Notification | boolean
  type Delta (line 53) | interface Delta {
  type ValuesDelta (line 62) | type ValuesDelta = Delta
  type MetaDelta (line 67) | type MetaDelta = Delta
  type Update (line 70) | type Update = {
  function hasValues (line 79) | function hasValues(u: Update): u is Update & { values: PathValue[] } {
  function hasMeta (line 84) | function hasMeta(u: Update): u is Update & { meta: Meta[] } {
  type PathValue (line 90) | interface PathValue {
  type Notification (line 97) | interface Notification {
  type Meta (line 109) | interface Meta {
  type MetaValue (line 116) | interface MetaValue {
  type ALARM_STATE (line 140) | enum ALARM_STATE {
  type ALARM_METHOD (line 150) | enum ALARM_METHOD {
  type AlarmStatus (line 156) | interface AlarmStatus {
  type Zone (line 165) | interface Zone {

FILE: packages/server-api/src/features.ts
  type WithFeatures (line 4) | interface WithFeatures {
  type FeatureInfo (line 57) | interface FeatureInfo {
  type SignalKApiId (line 68) | type SignalKApiId =

FILE: packages/server-api/src/history.ts
  type AggregateMethod (line 17) | type AggregateMethod =
  type ValueList (line 28) | type ValueList = {
  type DataRow (line 37) | type DataRow = [Timestamp, ...unknown[]]
  type ValuesResponse (line 39) | interface ValuesResponse {
  type TimeRangeQueryParams (line 71) | type TimeRangeQueryParams =
  type ValuesRequestQueryParams (line 103) | type ValuesRequestQueryParams = TimeRangeQueryParams & {
  type PathsRequestQueryParams (line 108) | type PathsRequestQueryParams = TimeRangeQueryParams
  type PathsResponse (line 109) | type PathsResponse = Path[]
  type ContextsRequestQueryParams (line 111) | type ContextsRequestQueryParams = TimeRangeQueryParams
  type ContextsResponse (line 112) | type ContextsResponse = Context[]
  type HistoryProviderRegistry (line 115) | type HistoryProviderRegistry = {
  type HistoryApiRegistry (line 124) | type HistoryApiRegistry = HistoryProviderRegistry
  type WithHistoryApi (line 126) | type WithHistoryApi = {
  type HistoryProvider (line 148) | type HistoryProvider = HistoryApi
  type HistoryApi (line 151) | interface HistoryApi {
  function isHistoryProvider (line 180) | function isHistoryProvider(obj: unknown): obj is HistoryProvider {
  type HistoryProviders (line 200) | interface HistoryProviders {
  type Duration (line 216) | type Duration = Temporal.Duration | number
  type TimeRangeParams (line 218) | type TimeRangeParams =
  type PathSpec (line 250) | interface PathSpec {
  type ValuesRequest (line 256) | type ValuesRequest = TimeRangeParams & {
  type PathsRequest (line 262) | type PathsRequest = TimeRangeParams
  type ContextsRequest (line 263) | type ContextsRequest = TimeRangeParams

FILE: packages/server-api/src/index.ts
  type Position (line 23) | interface Position {
  type RelativePositionOrigin (line 30) | interface RelativePositionOrigin {
  type SKVersion (line 36) | enum SKVersion {

FILE: packages/server-api/src/mmsi/mid.ts
  type FlagCountry (line 7) | interface FlagCountry {
  type Mid2FlagCountries (line 18) | type Mid2FlagCountries = { [mid: string]: FlagCountry }
  constant MID (line 21) | const MID: Mid2FlagCountries = Object.entries({

FILE: packages/server-api/src/mmsi/mmsi.ts
  type MMSISourceType (line 6) | type MMSISourceType =
  type MMSIInfo (line 19) | interface MMSIInfo {

FILE: packages/server-api/src/notificationsapi.ts
  type NotificationsApi (line 7) | interface NotificationsApi {
  type WithNotificationsApi (line 58) | interface WithNotificationsApi {
  type AlarmOptions (line 65) | interface AlarmOptions {

FILE: packages/server-api/src/plugin.ts
  type PluginConstructor (line 9) | type PluginConstructor = (app: ServerAPI) => Plugin
  type Plugin (line 55) | interface Plugin {

FILE: packages/server-api/src/propertyvalues.ts
  type PropertyValuesEmitter (line 38) | interface PropertyValuesEmitter {
  type PropertyValue (line 54) | interface PropertyValue {
  type PropertyValuesCallback (line 70) | type PropertyValuesCallback = (
  type StreamTuple (line 75) | interface StreamTuple {
  class PropertyValues (line 81) | class PropertyValues {
    method onPropertyValues (line 89) | onPropertyValues(propName: string, cb: PropertyValuesCallback): () => ...
    method emitPropertyValue (line 93) | emitPropertyValue(pv: PropertyValue) {
    method getStreamTuple (line 104) | private getStreamTuple(propName: string): StreamTuple {
  type Unsubscribe (line 129) | type Unsubscribe = () => void

FILE: packages/server-api/src/radarapi.ts
  type RadarStatus (line 13) | type RadarStatus = 'off' | 'standby' | 'transmit' | 'warming'
  type RadarControlValue (line 20) | interface RadarControlValue {
  type RadarControls (line 26) | interface RadarControls {
  type LegendEntry (line 38) | interface LegendEntry {
  type SupportedFeature (line 57) | type SupportedFeature = 'arpa' | 'guardZones' | 'trails' | 'dualRange'
  type RadarCharacteristics (line 64) | interface RadarCharacteristics {
  type ControlDefinitionV5 (line 90) | interface ControlDefinitionV5 {
  type ControlConstraint (line 142) | interface ControlConstraint {
  type CapabilityManifest (line 189) | interface CapabilityManifest {
  type RadarState (line 249) | interface RadarState {
  type ArpaTargetStatus (line 279) | type ArpaTargetStatus = 'tracking' | 'lost' | 'acquiring'
  type ArpaAcquisitionMethod (line 286) | type ArpaAcquisitionMethod = 'manual' | 'auto'
  type ArpaTargetPosition (line 293) | interface ArpaTargetPosition {
  type ArpaTargetMotion (line 309) | interface ArpaTargetMotion {
  type ArpaTargetDanger (line 321) | interface ArpaTargetDanger {
  type ArpaTarget (line 358) | interface ArpaTarget {
  type TargetListResponse (line 394) | interface TargetListResponse {
  type TargetStreamMessage (line 408) | interface TargetStreamMessage {
  type ArpaSettings (line 422) | interface ArpaSettings {
  type RadarInfo (line 464) | interface RadarInfo {
  type RadarProvider (line 529) | interface RadarProvider {
  type RadarProviderMethods (line 537) | interface RadarProviderMethods {
  type RadarApi (line 731) | interface RadarApi {
  type RadarProviderRegistry (line 747) | interface RadarProviderRegistry {
  type WithRadarApi (line 781) | type WithRadarApi = {
  type RadarProviders (line 797) | interface RadarProviders {

FILE: packages/server-api/src/resourcesapi.ts
  type SignalKResourceType (line 2) | type SignalKResourceType =
  constant SIGNALKRESOURCETYPES (line 12) | const SIGNALKRESOURCETYPES: SignalKResourceType[] = [
  type ResourceType (line 24) | type ResourceType = SignalKResourceType | string
  type ResourcesApi (line 27) | interface ResourcesApi {
  type WithResourcesApi (line 170) | interface WithResourcesApi {
  type ResourceProvider (line 175) | interface ResourceProvider {
  type ResourceProviderMethods (line 188) | interface ResourceProviderMethods {
  type ResourceProviderRegistry (line 391) | interface ResourceProviderRegistry {

FILE: packages/server-api/src/resourcetypes.ts
  type Resource (line 6) | type Resource<T> = T & {
  type Route (line 12) | interface Route {
  type Waypoint (line 30) | interface Waypoint {
  type Note (line 46) | interface Note {
  type Region (line 57) | interface Region {
  type Chart (line 64) | interface Chart {
  type GeoJsonPoint (line 79) | type GeoJsonPoint = [number, number, number?]
  type GeoJsonLinestring (line 81) | type GeoJsonLinestring = GeoJsonPoint[]
  type GeoJsonPolygon (line 83) | type GeoJsonPolygon = GeoJsonLinestring[]
  type GeoJsonMultiPolygon (line 85) | type GeoJsonMultiPolygon = GeoJsonPolygon[]
  type Polygon (line 88) | interface Polygon {
  type MultiPolygon (line 99) | interface MultiPolygon {

FILE: packages/server-api/src/serverapi.ts
  type ServerAPI (line 33) | interface ServerAPI
  type PluginServerApp (line 458) | type PluginServerApp = ServerAPI
  type DeltaInputHandler (line 461) | type DeltaInputHandler = (
  type Ports (line 467) | interface Ports {
  type SelfIdentity (line 476) | interface SelfIdentity {
  type Metadata (line 483) | interface Metadata {
  type ActionHandler (line 496) | type ActionHandler = (
  type ActionResult (line 504) | interface ActionResult {

FILE: packages/server-api/src/streambundle.ts
  type StreamBundle (line 5) | interface StreamBundle {

FILE: packages/server-api/src/subscriptionmanager.ts
  type SubscriptionManager (line 5) | interface SubscriptionManager {
  type SubscribeCallback (line 18) | type SubscribeCallback = (delta: Delta) => void
  type Unsubscribes (line 21) | type Unsubscribes = Array<() => void>
  type SubscribeMessage (line 29) | interface SubscribeMessage {
  type FixedPolicyOptions (line 51) | type FixedPolicyOptions = {
  type InstantPolicyOptions (line 71) | type InstantPolicyOptions = {
  type SubscriptionOptions (line 82) | type SubscriptionOptions = (
  type UnsubscribeMessage (line 101) | interface UnsubscribeMessage {

FILE: packages/server-api/src/typebox/autopilot-schemas.ts
  type AutopilotInfoType (line 87) | type AutopilotInfoType = Static<typeof AutopilotInfoSchema>
  type AngleInput (line 108) | type AngleInput = Static<typeof AngleInputSchema>

FILE: packages/server-api/src/typebox/course-schemas.ts
  type ArrivalCircleType (line 41) | type ArrivalCircleType = Static<typeof ArrivalCircleSchema>
  type PositionType (line 43) | type PositionType = Static<typeof PositionSchema>
  type HrefDestinationType (line 70) | type HrefDestinationType = Static<typeof HrefDestinationSchema>
  type PositionDestinationType (line 79) | type PositionDestinationType = Static<typeof PositionDestinationSchema>
  type SetDestinationBodyType (line 94) | type SetDestinationBodyType = Static<typeof SetDestinationBodySchema>
  type RouteDestinationType (line 119) | type RouteDestinationType = Static<typeof RouteDestinationSchema>
  type ArrivalCircleBodyType (line 128) | type ArrivalCircleBodyType = Static<typeof ArrivalCircleBodySchema>
  type TargetArrivalTimeBodyType (line 143) | type TargetArrivalTimeBodyType = Static<
  type NextPointBodyType (line 159) | type NextPointBodyType = Static<typeof NextPointBodySchema>
  type PointIndexBodyType (line 172) | type PointIndexBodyType = Static<typeof PointIndexBodySchema>
  type ReverseBodyType (line 187) | type ReverseBodyType = Static<typeof ReverseBodySchema>
  type ActiveRouteType (line 212) | type ActiveRouteType = Static<typeof ActiveRouteSchema>
  type NextPreviousPointType (line 229) | type NextPreviousPointType = Static<typeof NextPreviousPointSchema>
  type CourseInfoType (line 268) | type CourseInfoType = Static<typeof CourseInfoSchema>
  type CourseCalculationsType (line 413) | type CourseCalculationsType = Static<typeof CourseCalculationsSchema>

FILE: packages/server-api/src/typebox/discovery-schemas.ts
  type DiscoveryData (line 61) | type DiscoveryData = Static<typeof DiscoveryDataSchema>
  type PluginMetaData (line 77) | type PluginMetaData = Static<typeof PluginMetaDataSchema>
  type FeaturesModel (line 96) | type FeaturesModel = Static<typeof FeaturesModelSchema>

FILE: packages/server-api/src/typebox/history-schemas.ts
  type AggregateMethodSchemaType (line 25) | type AggregateMethodSchemaType = Static<typeof AggregateMethodSchema>
  type ValuesResponseSchemaType (line 74) | type ValuesResponseSchemaType = Static<typeof ValuesResponseSchema>
  type PathSpecSchemaType (line 91) | type PathSpecSchemaType = Static<typeof PathSpecSchema>
  type HistoryProviderInfoSchemaType (line 101) | type HistoryProviderInfoSchemaType = Static<
  type HistoryProvidersResponseSchemaType (line 113) | type HistoryProvidersResponseSchemaType = Static<

FILE: packages/server-api/src/typebox/notifications-schemas.ts
  type Alarm (line 47) | type Alarm = Static<typeof AlarmSchema>
  type NotificationResponse (line 61) | type NotificationResponse = Static<typeof NotificationResponseSchema>

FILE: packages/server-api/src/typebox/protocol-schemas.ts
  type ALARM_STATE (line 26) | enum ALARM_STATE {
  type ALARM_METHOD (line 40) | enum ALARM_METHOD {
  type Zone (line 107) | type Zone = Static<typeof ZoneSchema>
  type AlarmStatus (line 135) | type AlarmStatus = Static<typeof AlarmStatusSchema>
  type DisplayUnitsMetadata (line 162) | type DisplayUnitsMetadata = Static<typeof DisplayUnitsMetadataSchema>
  type EnhancedDisplayUnits (line 199) | type EnhancedDisplayUnits = Static<typeof EnhancedDisplayUnitsSchema>
  type MetaValue (line 260) | type MetaValue = Static<typeof MetaValueSchema>
  type Meta (line 275) | type Meta = Static<typeof MetaSchema>
  type Source (line 356) | type Source = Static<typeof SourceSchema>
  type Notification (line 396) | type Notification = Static<typeof NotificationSchema>
  type PathValue (line 411) | type PathValue = Static<typeof PathValueSchema>

FILE: packages/server-api/src/typebox/radar-schemas.ts
  type RadarStatusSchemaType (line 19) | type RadarStatusSchemaType = Static<typeof RadarStatusSchema>
  type RadarControlValueSchemaType (line 36) | type RadarControlValueSchemaType = Static<typeof RadarControlValueSchema>
  type RadarControlsSchemaType (line 70) | type RadarControlsSchemaType = Static<typeof RadarControlsSchema>
  type RadarInfoSchemaType (line 103) | type RadarInfoSchemaType = Static<typeof RadarInfoSchema>

FILE: packages/server-api/src/typebox/resources-schemas.ts
  type RouteResource (line 110) | type RouteResource = Static<typeof RouteSchema>
  type WaypointResource (line 142) | type WaypointResource = Static<typeof WaypointSchema>
  type RegionResource (line 172) | type RegionResource = Static<typeof RegionSchema>
  type NoteResource (line 217) | type NoteResource = Static<typeof NoteSchema>
  type ChartResource (line 317) | type ChartResource = Static<typeof ChartSchema>

FILE: packages/server-api/src/typebox/shared-schemas.ts
  type IsoTimeType (line 31) | type IsoTimeType = Static<typeof IsoTimeSchema>
  type Position (line 95) | type Position = Static<typeof PositionSchema>
  type RelativePositionOrigin (line 116) | type RelativePositionOrigin = Static<typeof RelativePositionOriginSchema>
  type GeoJsonPointGeometry (line 132) | type GeoJsonPointGeometry = Static<typeof GeoJsonPointGeometrySchema>
  type GeoJsonLinestringGeometry (line 151) | type GeoJsonLinestringGeometry = Static<
  type GeoJsonPolygonGeometry (line 174) | type GeoJsonPolygonGeometry = Static<typeof GeoJsonPolygonGeometrySchema>
  type GeoJsonMultiPolygonGeometry (line 197) | type GeoJsonMultiPolygonGeometry = Static<

FILE: packages/server-api/src/typebox/weather-schemas.ts
  type WeatherDataModel (line 286) | type WeatherDataModel = Static<typeof WeatherDataModelSchema>
  type WeatherWarningModel (line 316) | type WeatherWarningModel = Static<typeof WeatherWarningModelSchema>

FILE: packages/server-api/src/weatherapi.guard.ts
  function isWeatherProvider (line 7) | function isWeatherProvider(obj: unknown): obj is WeatherProvider {

FILE: packages/server-api/src/weatherapi.ts
  type WeatherApi (line 4) | interface WeatherApi {
  type WeatherProviderRegistry (line 112) | interface WeatherProviderRegistry {
  type WeatherProviders (line 132) | interface WeatherProviders {
  type WeatherProvider (line 164) | interface WeatherProvider {
  type WeatherProviderMethods (line 170) | interface WeatherProviderMethods {
  type WeatherWarning (line 279) | interface WeatherWarning {
  type WeatherReqParams (line 295) | interface WeatherReqParams {
  type WeatherForecastType (line 304) | type WeatherForecastType = 'daily' | 'point'
  type WeatherDataType (line 308) | type WeatherDataType = WeatherForecastType | 'observation'
  type WeatherData (line 314) | interface WeatherData {
  type TendencyKind (line 363) | type TendencyKind =
  type PrecipitationKind (line 372) | type PrecipitationKind =

FILE: packages/streams/src/autodetect.test.ts
  function createAutodetectApp (line 6) | function createAutodetectApp() {
  constant SK_DELTA (line 20) | const SK_DELTA = JSON.stringify({
  constant MUX_DELTA (line 30) | const MUX_DELTA = `${Date.now()};I;${SK_DELTA}`

FILE: packages/streams/src/autodetect.ts
  type AutodetectOptions (line 46) | interface AutodetectOptions {
  type TimestampedMessage (line 63) | interface TimestampedMessage {
  type DeltaMessage (line 70) | interface DeltaMessage {
  class ToTimestamped (line 75) | class ToTimestamped extends Transform {
    method constructor (line 80) | constructor(deMultiplexer: DeMultiplexer, options: AutodetectOptions) {
    method _transform (line 86) | _transform(
    method handleMixed (line 114) | private handleMixed(
    method handleMultiplexed (line 139) | private handleMultiplexed(
  class Splitter (line 155) | class Splitter extends Transform {
    method constructor (line 161) | constructor(deMultiplexer: DeMultiplexer, options: AutodetectOptions) {
    method _transform (line 181) | _transform(
    method pipe (line 229) | pipe<T extends NodeJS.WritableStream>(target: T): T {
  class DeMultiplexer (line 236) | class DeMultiplexer extends Writable {
    method constructor (line 241) | constructor(options: AutodetectOptions) {
    method pipe (line 255) | pipe<T extends NodeJS.WritableStream>(target: T): T {
    method write (line 259) | write(

FILE: packages/streams/src/canboatjs.ts
  type CanboatJsOptions (line 5) | interface CanboatJsOptions {
  type FileChunk (line 15) | interface FileChunk {
  class CanboatJs (line 21) | class CanboatJs extends Transform {
    method constructor (line 26) | constructor(options: CanboatJsOptions) {
    method _transform (line 51) | _transform(

FILE: packages/streams/src/execute.test.ts
  function createCollectingWritable (line 6) | function createCollectingWritable(): Writable & { chunks: string[] } {

FILE: packages/streams/src/execute.ts
  type ExecuteOptions (line 7) | interface ExecuteOptions {
  class Execute (line 25) | class Execute extends Transform {
    method constructor (line 33) | constructor(options: ExecuteOptions) {
    method _transform (line 40) | _transform(
    method pipe (line 49) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
    method end (line 78) | end(): this {
    method startProcess (line 88) | private startProcess(command: string): void {

FILE: packages/streams/src/filestream.ts
  class EndIgnoringPassThrough (line 4) | class EndIgnoringPassThrough extends PassThrough {
    method end (line 5) | end(): this {
  type FileStreamOptions (line 10) | interface FileStreamOptions {
  class FileStream (line 19) | class FileStream {
    method constructor (line 26) | constructor(options: FileStreamOptions) {
    method pipe (line 31) | pipe<T extends Writable>(pipeTo: T): T {
    method startStream (line 39) | startStream(): void {
    method end (line 61) | end(): void {

FILE: packages/streams/src/folderstream.ts
  type FolderStreamOptions (line 4) | interface FolderStreamOptions {
  class FolderStream (line 8) | class FolderStream extends Transform {
    method constructor (line 12) | constructor(options: FolderStreamOptions) {
    method pipe (line 17) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {

FILE: packages/streams/src/from_json.ts
  class FromJson (line 3) | class FromJson extends Transform {
    method constructor (line 4) | constructor() {
    method _transform (line 8) | _transform(

FILE: packages/streams/src/gpiod-seatalk.ts
  type GpiodSeatalkOptions (line 369) | interface GpiodSeatalkOptions {
  class GpiodSeatalk (line 383) | class GpiodSeatalk extends Execute {
    method constructor (line 384) | constructor(options: GpiodSeatalkOptions) {

FILE: packages/streams/src/gpsd.test.ts
  method write (line 58) | write(_chunk, _encoding, callback) {
  method write (line 95) | write(_chunk, _encoding, callback) {

FILE: packages/streams/src/gpsd.ts
  constant GPSD_DEFAULT_PORT (line 6) | const GPSD_DEFAULT_PORT = 2947
  constant GPSD_WATCH_COMMAND (line 7) | const GPSD_WATCH_COMMAND = '?WATCH={"class":"WATCH","nmea":true,"json":f...
  type GpsdOptions (line 9) | interface GpsdOptions {
  class Gpsd (line 23) | class Gpsd extends Transform {
    method constructor (line 31) | constructor(options: GpsdOptions) {
    method pipe (line 42) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
    method end (line 108) | end(): this {
    method _transform (line 115) | _transform(

FILE: packages/streams/src/keys-filter.ts
  type KeysFilterOptions (line 4) | interface KeysFilterOptions {
  type DeltaUpdate (line 9) | interface DeltaUpdate {
  type Delta (line 16) | interface Delta {
  class KeysFilter (line 21) | class KeysFilter extends Transform {
    method constructor (line 25) | constructor(options: KeysFilterOptions) {
    method _transform (line 32) | _transform(

FILE: packages/streams/src/liner.ts
  type LinerOptions (line 3) | interface LinerOptions {
  class Liner (line 8) | class Liner extends Transform {
    method constructor (line 12) | constructor(options: LinerOptions = {}) {
    method _transform (line 17) | _transform(
    method _flush (line 44) | _flush(done: TransformCallback): void {

FILE: packages/streams/src/log.ts
  type LogOptions (line 5) | interface LogOptions {
  class Log (line 12) | class Log extends Transform {
    method constructor (line 15) | constructor(options: LogOptions) {
    method _transform (line 20) | _transform(

FILE: packages/streams/src/logging.test.ts
  function createLoggingApp (line 8) | function createLoggingApp(

FILE: packages/streams/src/logging.ts
  type LoggingApp (line 22) | interface LoggingApp {
  type LogMessage (line 40) | interface LogMessage {
  class FileTimestampStreamWithDelete (line 44) | class FileTimestampStreamWithDelete extends FileTimestampStream {
    method constructor (line 50) | constructor(
    method newFilename (line 64) | newFilename(): string {
    method deleteOldFiles (line 72) | private deleteOldFiles(): void {
  function getLogger (line 101) | function getLogger(
  function getFullLogDir (line 153) | function getFullLogDir(app: LoggingApp, logdir?: string): string {
  function listLogFiles (line 159) | function listLogFiles(

FILE: packages/streams/src/mdns-ws.test.ts
  type DeltaChunk (line 7) | interface DeltaChunk {
  constant SK_HELLO (line 14) | const SK_HELLO = JSON.stringify({
  type SkServer (line 21) | type SkServer = {
  function createSkWsServer (line 27) | function createSkWsServer(fixedPort = 0): Promise<SkServer> {
  function createSink (line 55) | function createSink(): {
  function track (line 76) | function track<T extends ReturnType<typeof setInterval>>(id: T): T {
  function trackServer (line 81) | function trackServer(s: SkServer): SkServer {
  function trackMdns (line 86) | function trackMdns(m: MdnsWs): MdnsWs {

FILE: packages/streams/src/mdns-ws.ts
  type MdnsWsOptions (line 24) | interface MdnsWsOptions {
  type DeltaMessage (line 46) | interface DeltaMessage {
  class MdnsWs (line 51) | class MdnsWs extends Transform {
    method constructor (line 64) | constructor(options: MdnsWsOptions) {
    method verifyRemoteToken (line 135) | private verifyRemoteToken(): Promise<boolean> {
    method setProviderStatus (line 160) | private setProviderStatus(message: string, isError: boolean): void {
    method connectClient (line 168) | private connectClient(client: Client): void {
    method fetchMetaIfNeeded (line 286) | private fetchMetaIfNeeded(
    method _transform (line 312) | _transform(
    method _destroy (line 320) | _destroy(

FILE: packages/streams/src/n2k-signalk.test.ts
  constant HEADING_PGN (line 5) | const HEADING_PGN = {

FILE: packages/streams/src/n2k-signalk.ts
  type N2kFilter (line 22) | interface N2kFilter {
  type N2kToSignalKOptions (line 27) | interface N2kToSignalKOptions {
  type N2kMessage (line 44) | interface N2kMessage {
  type DeltaSource (line 50) | interface DeltaSource {
  type DeltaValue (line 58) | interface DeltaValue {
  type DeltaUpdate (line 63) | interface DeltaUpdate {
  type Delta (line 69) | interface Delta {
  type SourceMeta (line 74) | interface SourceMeta {
  type NotificationEntry (line 79) | interface NotificationEntry {
  class N2kToSignalK (line 84) | class N2kToSignalK extends Transform {
    method constructor (line 95) | constructor(options: N2kToSignalKOptions) {
    method isFiltered (line 183) | private isFiltered(source: DeltaSource): N2kFilter | undefined {
    method _transform (line 200) | _transform(

FILE: packages/streams/src/n2kAnalyzer.ts
  type N2kAnalyzerOptions (line 4) | interface N2kAnalyzerOptions {
  type AnalyzerOutput (line 12) | interface AnalyzerOutput {
  class N2kAnalyzer (line 17) | class N2kAnalyzer extends Transform {
    method constructor (line 22) | constructor(options: N2kAnalyzerOptions) {
    method _transform (line 64) | _transform(
    method pipe (line 73) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
    method end (line 78) | end(): this {

FILE: packages/streams/src/nmea0183-signalk.test.ts
  function createNmeaApp (line 7) | function createNmeaApp() {
  constant RMC_SENTENCE (line 23) | const RMC_SENTENCE =

FILE: packages/streams/src/nmea0183-signalk.ts
  function isN2KOver0183 (line 22) | function isN2KOver0183(msg: string): boolean {
  type Nmea0183ToSignalKOptions (line 29) | interface Nmea0183ToSignalKOptions {
  type TimestampedChunk (line 42) | interface TimestampedChunk {
  type DeltaUpdate (line 47) | interface DeltaUpdate {
  type Delta (line 52) | interface Delta {
  type N2kToDelta (line 56) | type N2kToDelta = (
  class Nmea0183ToSignalK (line 62) | class Nmea0183ToSignalK extends Transform {
    method constructor (line 95) | constructor(options: Nmea0183ToSignalKOptions) {
    method _transform (line 118) | _transform(

FILE: packages/streams/src/nullprovider.ts
  class NullProvider (line 3) | class NullProvider extends Transform {
    method constructor (line 4) | constructor() {

FILE: packages/streams/src/pigpio-seatalk.ts
  type PigpioSeatalkOptions (line 86) | interface PigpioSeatalkOptions {
  class PigpioSeatalk (line 100) | class PigpioSeatalk extends Execute {
    method constructor (line 101) | constructor(options: PigpioSeatalkOptions) {

FILE: packages/streams/src/replacer.ts
  type ReplacerOptions (line 3) | interface ReplacerOptions {
  class Replacer (line 8) | class Replacer extends Transform {
    method constructor (line 12) | constructor(options: ReplacerOptions) {
    method _transform (line 18) | _transform(

FILE: packages/streams/src/s3.ts
  type S3ProviderOptions (line 28) | interface S3ProviderOptions {
  class S3Provider (line 33) | class S3Provider extends Transform {
    method constructor (line 37) | constructor({ bucket, prefix }: S3ProviderOptions) {
    method pipe (line 43) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {

FILE: packages/streams/src/serialport.ts
  type SerialStreamOptions (line 6) | interface SerialStreamOptions {
  class SerialStream (line 24) | class SerialStream extends Transform {
    method constructor (line 34) | constructor(options: SerialStreamOptions) {
    method start (line 91) | start(): void {
    method end (line 147) | end(): this {
    method _transform (line 158) | _transform(
    method scheduleReconnect (line 167) | private scheduleReconnect(): void {

FILE: packages/streams/src/simple.ts
  type CanboatCtor (line 24) | interface CanboatCtor {
  function requireN2K (line 30) | function requireN2K(): {
  function requireN2kToSignalK (line 42) | function requireN2kToSignalK(): new (options: object) => PipeElement {
  function requireNmea0183ToSignalK (line 47) | function requireNmea0183ToSignalK(): new (options: object) => PipeElement {
  function requireW2k01 (line 52) | function requireW2k01(): CanboatCtor {
  type SimpleApp (line 57) | interface SimpleApp {
  type SubOptions (line 81) | interface SubOptions {
  type SimpleOptions (line 113) | interface SimpleOptions {
  type PipeElement (line 126) | interface PipeElement {
  type PipelineFactory (line 134) | type PipelineFactory = (options: SimpleOptions) => PipeElement[]
  type PipeStartFactory (line 135) | type PipeStartFactory = (
  function nmea2000input (line 268) | function nmea2000input(
  function nmea0183input (line 394) | function nmea0183input(subOptions: SubOptions): PipeElement[] {
  function executeInput (line 429) | function executeInput(subOptions: SubOptions): PipeElement[] {
  function fileInput (line 436) | function fileInput(subOptions: SubOptions): PipeElement[] {
  function signalKInput (line 443) | function signalKInput(subOptions: SubOptions): PipeElement[] {
  function seatalkInput (line 467) | function seatalkInput(subOptions: SubOptions): PipeElement[] {
  function nmea0183inputFilter (line 487) | function nmea0183inputFilter(ignoredSentences: string[]): PipeElement[] {
  function seatalk1inputFilter (line 505) | function seatalk1inputFilter(ignoredCommands: string[]): PipeElement[] {
  function getLoggerPipeline (line 526) | function getLoggerPipeline(
  class Simple (line 542) | class Simple extends Transform {
    method constructor (line 545) | constructor(options: SimpleOptions) {
    method _transform (line 678) | _transform(
    method end (line 687) | end(): this {

FILE: packages/streams/src/splitting-liner.ts
  type SplittingLinerOptions (line 3) | interface SplittingLinerOptions {
  class SplittingLiner (line 8) | class SplittingLiner extends Transform {
    method constructor (line 11) | constructor(options: SplittingLinerOptions = {}) {
    method _transform (line 16) | _transform(

FILE: packages/streams/src/tcp.test.ts
  function createCollectingWritable (line 8) | function createCollectingWritable(): Writable & { chunks: string[] } {

FILE: packages/streams/src/tcp.ts
  constant BUFFER_LIMIT (line 6) | const BUFFER_LIMIT = process.env.BACKPRESSURE_ENTER
  type TcpOptions (line 10) | interface TcpOptions {
  class TcpStream (line 28) | class TcpStream extends Transform {
    method constructor (line 37) | constructor(options: TcpOptions) {
    method pipe (line 53) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
    method end (line 153) | end(): this {
    method _transform (line 160) | _transform(

FILE: packages/streams/src/tcpserver.ts
  type TcpServerOptions (line 3) | interface TcpServerOptions {
  class TcpServer (line 10) | class TcpServer extends Transform {
    method constructor (line 13) | constructor(options: TcpServerOptions) {
    method pipe (line 18) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
    method _transform (line 23) | _transform(

FILE: packages/streams/src/test-helpers.ts
  type MockAppOptions (line 4) | interface MockAppOptions {
  type MockApp (line 13) | interface MockApp extends EventEmitter {
  function createMockApp (line 38) | function createMockApp(options: MockAppOptions = {}): MockApp {
  function collectStreamOutput (line 92) | function collectStreamOutput<T = unknown>(
  function noopDebug (line 103) | function noopDebug(): DebugLogger {
  function createDebugStub (line 109) | function createDebugStub(): CreateDebug {

FILE: packages/streams/src/timestamp-throttle.ts
  type TimestampMessage (line 4) | interface TimestampMessage {
  type GetMilliseconds (line 8) | type GetMilliseconds = (msg: TimestampMessage) => number
  type TimestampThrottleOptions (line 10) | interface TimestampThrottleOptions {
  function defaultGetMilliseconds (line 14) | function defaultGetMilliseconds(msg: TimestampMessage): number {
  class TimestampThrottle (line 19) | class TimestampThrottle extends Transform {
    method constructor (line 24) | constructor(options: TimestampThrottleOptions = {}) {
    method _transform (line 30) | _transform(

FILE: packages/streams/src/types.ts
  type DebugLogger (line 1) | type DebugLogger = ((...args: unknown[]) => void) & {
  type CreateDebug (line 5) | type CreateDebug = (namespace: string) => DebugLogger
  type DeltaCache (line 7) | interface DeltaCache {

FILE: packages/streams/src/udp.test.ts
  function createCollectingWritable (line 7) | function createCollectingWritable(): Writable & { chunks: string[] } {

FILE: packages/streams/src/udp.ts
  type UdpOptions (line 5) | interface UdpOptions {
  class Udp (line 20) | class Udp extends Transform {
    method constructor (line 26) | constructor(options: UdpOptions) {
    method pipe (line 33) | pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
    method _transform (line 75) | _transform(
    method end (line 83) | end(): this {

FILE: packages/streams/src/vendor.d.ts
  class Throttle (line 3) | class Throttle extends Transform {
  type ConnectFunction (line 10) | type ConnectFunction<T> = (options: object) => T
  type Reconnector (line 11) | interface Reconnector<T> extends EventEmitter {
  type FileTimestampStreamOptions (line 23) | interface FileTimestampStreamOptions {
  class FileTimestampStream (line 27) | class FileTimestampStream extends Writable {
  class Parser (line 41) | class Parser {
  type ClientOptions (line 50) | interface ClientOptions {
  type Connection (line 61) | interface Connection {
  class Client (line 66) | class Client extends EventEmitter {
  type S3ListObjectsParams (line 82) | interface S3ListObjectsParams {
  type S3Object (line 86) | interface S3Object {
  type S3ListObjectsResult (line 89) | interface S3ListObjectsResult {
  type S3GetObjectParams (line 92) | interface S3GetObjectParams {
  type S3Request (line 96) | interface S3Request {
  class S3 (line 100) | class S3 {

FILE: packages/typedoc-theme/src/SignalKTheme.tsx
  class SignalKTheme (line 14) | class SignalKTheme extends DefaultTheme {
    method constructor (line 15) | constructor(renderer: Renderer) {
    method getRenderContext (line 85) | getRenderContext(pageEvent: PageEvent<Reflection>) {

FILE: packages/typedoc-theme/src/SignalKThemeContext.tsx
  class SignalKThemeContext (line 6) | class SignalKThemeContext extends DefaultThemeRenderContext {

FILE: packages/typedoc-theme/src/index.tsx
  function load (line 7) | function load(app: Application) {

FILE: src/@types/primus.d.ts
  type PrimusOptions (line 4) | interface PrimusOptions {
  class Primus (line 14) | class Primus {

FILE: src/@types/signalk_signalk-schema.d.ts
  class FullSignalK (line 8) | class FullSignalK extends EventEmitter {

FILE: src/BackpressureManager.ts
  constant DEFAULT_ENTER_THRESHOLD (line 11) | const DEFAULT_ENTER_THRESHOLD = 512 * 1024
  constant DEFAULT_EXIT_THRESHOLD (line 12) | const DEFAULT_EXIT_THRESHOLD = 1024
  constant DEFAULT_MAX_BUFFER_SIZE (line 13) | const DEFAULT_MAX_BUFFER_SIZE = 4 * 512 * 1024
  constant DEFAULT_MAX_BUFFER_CHECK_TIME (line 14) | const DEFAULT_MAX_BUFFER_CHECK_TIME = 30 * 1000
  type BackpressureTransport (line 16) | interface BackpressureTransport {
  type BackpressureOptions (line 23) | interface BackpressureOptions {
  type BackpressureThresholds (line 31) | interface BackpressureThresholds {
  function parseBackpressureThresholds (line 38) | function parseBackpressureThresholds(configFallbacks?: {
  class BackpressureManager (line 59) | class BackpressureManager {
    method constructor (line 67) | constructor(transport: BackpressureTransport, options: BackpressureOpt...
    method onDrain (line 72) | onDrain(): void {
    method send (line 80) | send(delta: Delta): void {
    method flush (line 100) | flush(): void {
    method assertBufferSize (line 119) | assertBufferSize(knownBufferLength?: number): void {
    method clear (line 143) | clear(): void {
    method isActive (line 150) | get isActive(): boolean {
    method accumulatorSize (line 154) | get accumulatorSize(): number {

FILE: src/LatestValuesAccumulator.ts
  type AccumulatedItem (line 16) | interface AccumulatedItem {
  type BackpressureDelta (line 24) | interface BackpressureDelta extends Delta {
  function accumulateLatestValue (line 38) | function accumulateLatestValue(
  function buildFlushDeltas (line 66) | function buildFlushDeltas(

FILE: src/api/autopilot/index.ts
  constant AUTOPILOT_API_PATH (line 26) | const AUTOPILOT_API_PATH = `/signalk/v2/api/vessels/self/autopilots`
  constant DEFAULTIDPATH (line 27) | const DEFAULTIDPATH = '_default'
  type AutopilotApplication (line 29) | interface AutopilotApplication
  type AutopilotList (line 32) | interface AutopilotList {
  type AutopilotApiSettings (line 36) | interface AutopilotApiSettings {
  class AutopilotApi (line 40) | class AutopilotApi {
    method constructor (line 51) | constructor(private server: AutopilotApplication) {}
    method start (line 53) | async start() {
    method register (line 61) | register(pluginId: string, provider: AutopilotProvider, devices: strin...
    method unRegister (line 91) | unRegister(pluginId: string) {
    method apUpdate (line 143) | apUpdate(
    method updateAllowed (line 195) | private updateAllowed(request: Request): boolean {
    method initApiEndpoints (line 204) | private initApiEndpoints() {
    method useProvider (line 715) | private useProvider(req: Request): AutopilotProvider {
    method getDevices (line 747) | private getDevices(): AutopilotList {
    method initDefaults (line 761) | private initDefaults(deviceId?: string) {
    method buildPathValue (line 796) | private buildPathValue(path: Path, value: Value): PathValue {
    method emitUpdates (line 804) | private emitUpdates(values: PathValue[], source: SourceRef) {

FILE: src/api/course/index.ts
  constant COURSE_API_SCHEMA (line 45) | const COURSE_API_SCHEMA = buildSchemaSync(courseApiDoc)
  constant SIGNALK_API_PATH (line 47) | const SIGNALK_API_PATH = `/signalk/v2/api`
  constant COURSE_API_PATH (line 48) | const COURSE_API_PATH = `${SIGNALK_API_PATH}/vessels/self/navigation/cou...
  constant API_CMD_SRC (line 50) | const API_CMD_SRC: CommandSource = {
  constant COURSE_API_V2_DELTA_COUNT (line 55) | const COURSE_API_V2_DELTA_COUNT = 13
  constant COURSE_API_V1_DELTA_COUNT (line 56) | const COURSE_API_V1_DELTA_COUNT = 8
  constant COURSE_API_INITIAL_DELTA_COUNT (line 57) | const COURSE_API_INITIAL_DELTA_COUNT =
  type CourseApplication (line 60) | interface CourseApplication
  type CommandSource (line 68) | interface CommandSource {
  constant NO_COURSE_INFO (line 74) | const NO_COURSE_INFO: CourseInfo = {
  class CourseApi (line 83) | class CourseApi {
    method constructor (line 90) | constructor(
    method start (line 98) | async start() {
    method processResourceDeltas (line 175) | private async processResourceDeltas(delta: Delta) {
    method parseSettings (line 250) | private parseSettings() {
    method saveSettings (line 270) | private saveSettings() {
    method processV1DestinationDeltas (line 283) | private async processV1DestinationDeltas(delta: Delta) {
    method isValidPosition (line 318) | private isValidPosition(position: Position): boolean {
    method parseStreamValue (line 333) | private async parseStreamValue(cmdSource: CommandSource, pos: Position) {
    method getCourse (line 375) | async getCourse(): Promise<CourseInfo> {
    method clearDestination (line 381) | async clearDestination(persistState?: boolean): Promise<void> {
    method destination (line 393) | async destination(
    method activeRoute (line 411) | async activeRoute(dest: RouteDestination | null) {
    method getVesselPosition (line 424) | private getVesselPosition() {
    method validateCourseInfo (line 428) | private async validateCourseInfo(info: CourseInfo) {
    method isValidRouteCourse (line 445) | private async isValidRouteCourse(info: CourseInfo): Promise<boolean> {
    method isValidWaypointCourse (line 458) | private async isValidWaypointCourse(info: CourseInfo): Promise<boolean> {
    method updateAllowed (line 473) | private updateAllowed(request: Request): boolean {
    method initCourseRoutes (line 482) | private initCourseRoutes() {
    method calcReversedIndex (line 843) | private calcReversedIndex(activeRoute: ActiveRoute): number {
    method activateRoute (line 851) | private async activateRoute(
    method setDestination (line 926) | private async setDestination(
    method isValidArrivalCircle (line 1008) | private isValidArrivalCircle(value: number | undefined): boolean {
    method isValidIsoTime (line 1012) | private isValidIsoTime(value: string | undefined): boolean {
    method parsePointIndex (line 1020) | private parsePointIndex(index: number, rte: any): number {
    method parseHref (line 1039) | private parseHref(
    method getRoutePoint (line 1059) | private getRoutePoint(rte: any, index: number, reverse: boolean | null) {
    method getRoutePoints (line 1075) | private getRoutePoints(rte: any) {
    method getRoute (line 1087) | private async getRoute(href: string): Promise<Route | undefined> {
    method buildDeltaMsg (line 1104) | private buildDeltaMsg(paths: string[]): any {
    method buildV1DeltaMsg (line 1155) | private buildV1DeltaMsg(paths: string[]): Delta {
    method emitCourseInfo (line 1246) | private emitCourseInfo(noSave?: boolean, ...paths: string[]) {

FILE: src/api/discovery/index.ts
  constant FEATURES_API_PATH (line 7) | const FEATURES_API_PATH = `/signalk/v2/features`
  type FeaturesApplication (line 9) | interface FeaturesApplication extends IRouter, WithFeatures {}
  class FeaturesApi (line 11) | class FeaturesApi {
    method constructor (line 12) | constructor(private app: FeaturesApplication) {}
    method start (line 14) | async start() {
    method initApiRoutes (line 21) | private initApiRoutes() {

FILE: src/api/history/index.ts
  constant HISTORY_API_PATH (line 28) | const HISTORY_API_PATH = `/signalk/v2/api/history`
  type HistoryApplication (line 30) | interface HistoryApplication extends WithSecurityStrategy, IRouter {}
  class HistoryApiHttpRegistry (line 32) | class HistoryApiHttpRegistry {
    method constructor (line 37) | constructor(private app: HistoryApplication & WithHistoryApi) {
    method registerHistoryApiProvider (line 65) | registerHistoryApiProvider(
    method unregisterHistoryApiProvider (line 85) | unregisterHistoryApiProvider(pluginId: string): void {
    method start (line 100) | start() {
    method defaultProvider (line 224) | private defaultProvider(): HistoryProvider {
    method useProvider (line 234) | private useProvider(req: Request): HistoryProvider | undefined {
  function respondWith (line 248) | async function respondWith<T>(

FILE: src/api/index.ts
  type ApiResponse (line 15) | interface ApiResponse {

FILE: src/api/notifications/alarm.ts
  type AlarmProperties (line 18) | interface AlarmProperties {
  class Alarm (line 24) | class Alarm {
    method constructor (line 52) | constructor(notificationId?: NotificationId) {
    method parseDelta (line 66) | private parseDelta(update: Update, context: Context) {
    method alignAlarmMethod (line 85) | private alignAlarmMethod() {
    method timeStamp (line 100) | private timeStamp() {
    method syncFromNotificationUpdate (line 109) | public syncFromNotificationUpdate(update: Update, context: Context) {
    method isExternal (line 146) | get isExternal(): boolean {
    method delta (line 154) | get delta(): Delta {
    method properties (line 177) | get properties(): AlarmProperties {
    method extKey (line 188) | get extKey(): string {
    method setPath (line 196) | public setPath(path: Path, id?: string) {
    method silence (line 206) | public silence() {
    method acknowledge (line 222) | public acknowledge() {
    method clear (line 237) | public clear() {

FILE: src/api/notifications/index.ts
  type NotificationApplication (line 26) | interface NotificationApplication
  constant SIGNALK_API_PATH (line 34) | const SIGNALK_API_PATH = `/signalk/v2/api`
  constant NOTI_API_PATH (line 35) | const NOTI_API_PATH = `${SIGNALK_API_PATH}/notifications`
  class NotificationApi (line 39) | class NotificationApi {
    method constructor (line 44) | constructor(private server: NotificationApplication) {
    method start (line 49) | async start() {
    method filterNotifications (line 62) | private filterNotifications(delta: Delta): Delta {
    method handleNotificationUpdate (line 103) | private handleNotificationUpdate(update: Update, context: Context) {
    method initNotificationRoutes (line 120) | private initNotificationRoutes() {
    method listNotifications (line 285) | listNotifications() {
    method getNotification (line 289) | getNotification(id: string) {
    method silenceNotification (line 293) | silenceNotification(id: string) {
    method silenceAll (line 297) | silenceAll() {
    method acknowledgeNotification (line 301) | acknowledgeNotification(id: string) {
    method acknowledgeAll (line 305) | acknowledgeAll() {
    method clearNotification (line 309) | clearNotification(id: string) {
    method raiseNotification (line 313) | raiseNotification(options: AlarmOptions) {
    method updateNotification (line 317) | updateNotification(id: string, options: AlarmOptions) {
    method mob (line 321) | mob(message: string) {

FILE: src/api/notifications/notificationManager.ts
  constant CLEAN_INTERVAL (line 21) | const CLEAN_INTERVAL = 60000
  type NotificationKey (line 23) | type NotificationKey = Brand<string, 'notificationKey'>
  class NotificationManager (line 43) | class NotificationManager {
    method constructor (line 51) | constructor(private server: NotificationApplication) {
    method emitNotification (line 61) | private emitNotification(alarm: Alarm) {
    method list (line 70) | get list(): Record<string, AlarmProperties> {
    method get (line 83) | get(id: NotificationId): AlarmProperties | undefined {
    method raise (line 92) | raise(options: AlarmOptions): NotificationId {
    method update (line 128) | update(id: NotificationId, options: AlarmOptions) {
    method mob (line 154) | mob(options?: { message: string }): NotificationId {
    method silenceAll (line 168) | silenceAll() {
    method silence (line 183) | silence(id: NotificationId) {
    method acknowledgeAll (line 195) | acknowledgeAll() {
    method acknowledge (line 206) | acknowledge(id: NotificationId) {
    method clear (line 219) | clear(id: NotificationId) {
    method processNotificationUpdate (line 233) | processNotificationUpdate(u: Update, context: Context) {
    method clean (line 254) | private clean() {

FILE: src/api/openApiSchemas.ts
  type SchemaObject (line 12) | type SchemaObject = Record<string, any>
  constant NON_STANDARD_PROPS (line 15) | const NON_STANDARD_PROPS = new Set(['units', 'additionalItems'])
  function sanitize (line 17) | function sanitize(obj: any): any {
  function typeboxToOpenApiSchemas (line 56) | function typeboxToOpenApiSchemas(

FILE: src/api/radar/index.ts
  constant RADAR_API_PATH (line 13) | const RADAR_API_PATH = `/signalk/v2/api/vessels/self/radars`
  constant TWO_PI (line 14) | const TWO_PI = 2 * Math.PI
  type RadarApplication (line 16) | interface RadarApplication
  class RadarApi (line 19) | class RadarApi {
    method constructor (line 23) | constructor(private app: RadarApplication) {}
    method start (line 25) | async start() {
    method register (line 35) | register(pluginId: string, provider: radar.RadarProvider) {
    method unRegister (line 59) | unRegister(pluginId: string) {
    method getRadars (line 92) | async getRadars(): Promise<radar.RadarInfo[]> {
    method getRadarInfo (line 113) | async getRadarInfo(radarId: string): Promise<radar.RadarInfo | null> {
    method updateAllowed (line 130) | private updateAllowed(request: Request): boolean {
    method findProviderForRadar (line 142) | private async findProviderForRadar(
    method initApiEndpoints (line 158) | private initApiEndpoints() {

FILE: src/api/resources/index.ts
  constant RESOURCES_API_PATH (line 24) | const RESOURCES_API_PATH = `/signalk/v2/api/resources`
  constant CHART_TILE_REGEX (line 26) | const CHART_TILE_REGEX = /\/charts\/[^?]+\/\d+\/\d+\/\d+$/
  type DefaultProviders (line 30) | interface DefaultProviders {
  type ResourceApplication (line 34) | interface ResourceApplication
  type ResourceSettings (line 37) | interface ResourceSettings {
  class ResourcesApi (line 41) | class ResourcesApi {
    method constructor (line 47) | constructor(app: ResourceApplication) {
    method start (line 53) | async start() {
    method parseSettings (line 59) | async parseSettings() {
    method saveSettings (line 85) | saveSettings() {
    method register (line 93) | register(pluginId: string, provider: ResourceProvider) {
    method unRegister (line 121) | unRegister(pluginId: string) {
    method isResourceProvider (line 154) | isResourceProvider(provider: ResourceProvider) {
    method getResource (line 167) | async getResource(
    method listResources (line 181) | async listResources(
    method setResource (line 197) | async setResource(
    method deleteResource (line 247) | async deleteResource(
    method hasRegisteredProvider (line 282) | private hasRegisteredProvider(resType: string): boolean {
    method getProviderForWrite (line 292) | async getProviderForWrite(
    method checkForProvider (line 342) | private checkForProvider(
    method listFromAll (line 364) | private async listFromAll(resType: string, params: { [key: string]: an...
    method getFromAll (line 391) | private async getFromAll(resType: string, resId: string, property?: st...
    method getProviderForResourceId (line 420) | private async getProviderForResourceId(
    method getProvidersForResourceType (line 463) | private getProvidersForResourceType(resType: string): Array<string> {
    method initResourceRoutes (line 471) | private initResourceRoutes(server: ResourceApplication) {
    method getResourcePaths (line 962) | private getResourcePaths(): { [key: string]: any } {
    method buildDeltaMsg (line 976) | private buildDeltaMsg(

FILE: src/api/resources/validate.ts
  class ValidationError (line 8) | class ValidationError extends Error {}
  constant API_SCHEMA (line 10) | const API_SCHEMA = buildSchemaSync(resourcesApiDoc)

FILE: src/api/streams/binary-stream-manager.ts
  constant MAX_WEBSOCKET_BUFFER_SIZE (line 16) | const MAX_WEBSOCKET_BUFFER_SIZE = 256 * 1024 // 256KB
  constant MAX_CONSECUTIVE_DROPS (line 21) | const MAX_CONSECUTIVE_DROPS = 30 // ~0.5 seconds at 60Hz
  constant MAX_BUFFERED_FRAMES (line 26) | const MAX_BUFFERED_FRAMES = 100
  type StreamPrincipal (line 31) | interface StreamPrincipal {
  type StreamClient (line 38) | interface StreamClient {
  class BinaryStreamManager (line 49) | class BinaryStreamManager {
    method emitData (line 62) | emitData(streamId: string, data: Buffer): void {
    method addClient (line 108) | addClient(streamId: string, ws: WebSocket, principal: StreamPrincipal)...
    method removeClient (line 141) | removeClient(streamId: string, ws: WebSocket): void {
    method cleanupStream (line 173) | cleanupStream(streamId: string): void {
    method sendToClient (line 196) | private sendToClient(client: StreamClient, data: Buffer): void {
    method getBufferSize (line 240) | getBufferSize(streamId: string): number {
    method getClientCount (line 251) | getClientCount(streamId: string): number {

FILE: src/api/streams/index.ts
  type WebSocketSecurityStrategy (line 24) | interface WebSocketSecurityStrategy extends SecurityStrategy {
  type WithServer (line 32) | interface WithServer {
  type StreamApplication (line 39) | interface StreamApplication
  type AuthenticatedRequest (line 47) | interface AuthenticatedRequest extends IncomingMessage {
  function initializeBinaryStreams (line 56) | function initializeBinaryStreams(app: StreamApplication): void {

FILE: src/api/swagger.ts
  type OpenApiDescription (line 25) | type OpenApiDescription = Brand<object, 'OpenApiDescription'>
  type OpenApiRecord (line 27) | interface OpenApiRecord {
  type ApiRecords (line 33) | interface ApiRecords {
  function mountSwaggerUi (line 53) | function mountSwaggerUi(app: IRouter & PluginManager, path: string) {
  function resolveRefs (line 220) | function resolveRefs(obj: any, root: any): void {
  function asyncApiViewerHtml (line 243) | function asyncApiViewerHtml(specsJson: string): string {

FILE: src/api/weather/index.ts
  constant WEATHER_API_PATH (line 22) | const WEATHER_API_PATH = `/signalk/v2/api/weather`
  type WeatherApplication (line 24) | interface WeatherApplication
  class WeatherApi (line 27) | class WeatherApi {
    method constructor (line 32) | constructor(private app: WeatherApplication) {}
    method start (line 34) | async start() {
    method register (line 42) | register(pluginId: string, provider: WeatherProvider) {
    method unRegister (line 64) | unRegister(pluginId: string) {
    method getObservations (line 99) | async getObservations(position: Position, options?: WeatherReqParams) {
    method getForecasts (line 123) | async getForecasts(
    method getWarnings (line 154) | async getWarnings(position: Position) {
    method updateAllowed (line 173) | private updateAllowed(request: Request): boolean {
    method checkLocation (line 183) | private checkLocation(req: Request): number {
    method parseRequest (line 193) | private parseRequest(req: Request, res: Response, next: NextFunction) {
    method initApiEndpoints (line 233) | private initApiEndpoints() {
    method useProvider (line 487) | private useProvider(req: Request): WeatherProviderMethods {
    method parseQueryOptions (line 519) | private parseQueryOptions(query: any): WeatherReqOptions {
    method parseValueAsNumber (line 549) | private parseValueAsNumber(value: unknown): number | undefined {
  type WeatherReqOptions (line 555) | interface WeatherReqOptions {

FILE: src/app.ts
  type ServerApp (line 9) | interface ServerApp extends ServerAPI {
  type SignalKMessageHub (line 28) | interface SignalKMessageHub extends EventEmitter {
  type WithConfig (line 37) | interface WithConfig {

FILE: src/atomicWrite.ts
  function atomicWriteFileSync (line 3) | function atomicWriteFileSync(filePath: string, data: string): void {
  function atomicWriteFile (line 16) | async function atomicWriteFile(

FILE: src/baconjs-compat.ts
  type ResolveFilename (line 27) | type ResolveFilename = (
  type Mappable (line 49) | type Mappable = { map: (f: unknown) => unknown }
  function patchMapShorthand (line 51) | function patchMapShorthand(proto: Mappable) {

FILE: src/categories.ts
  constant NEW_CATEGORY (line 22) | const NEW_CATEGORY = 'New/Updated'
  constant CAT_DEPRECATED (line 24) | const CAT_DEPRECATED = 'signalk-category-deprecated'
  function getCategories (line 30) | function getCategories(thePackage: NpmPackageData): string[] {
  function getAvailableCategories (line 63) | function getAvailableCategories() {
  constant CAT_KEYWORDS_TO_NAMES (line 69) | const CAT_KEYWORDS_TO_NAMES: {
  constant DEFAULT_MODULE_CAT_KEYWORDS (line 85) | const DEFAULT_MODULE_CAT_KEYWORDS: {

FILE: src/config/config.ts
  type Config (line 46) | interface Config {
  type ConfigApp (line 96) | interface ConfigApp extends ServerApp, WithConfig, SignalKMessageHub {
  function load (line 101) | function load(app: ConfigApp) {
  function checkPackageVersion (line 259) | function checkPackageVersion(name: string, pkg: any, appPath: string) {
  function getConfigDirectory (line 281) | function getConfigDirectory({ argv, config, env }: any) {
  function setConfigDirectory (line 299) | function setConfigDirectory(app: ConfigApp) {
  function getDefaultsPath (line 325) | function getDefaultsPath(app: ConfigApp) {
  function getBaseDeltasPath (line 333) | function getBaseDeltasPath(app: ConfigApp) {
  function readDefaultsFile (line 341) | function readDefaultsFile(app: ConfigApp) {
  function getFullDefaults (line 347) | function getFullDefaults(app: ConfigApp) {
  function setBaseDeltas (line 365) | function setBaseDeltas(app: ConfigApp) {
  function sendBaseDeltas (line 381) | function sendBaseDeltas(app: ConfigApp) {
  function writeDefaultsFile (line 388) | function writeDefaultsFile(app: ConfigApp, defaults: any, cb: any) {
  function writeBaseDeltasFileSync (line 394) | function writeBaseDeltasFileSync(app: ConfigApp) {
  function writeBaseDeltasFile (line 398) | function writeBaseDeltasFile(app: ConfigApp) {
  function setSelfSettings (line 402) | function setSelfSettings(app: ConfigApp) {
  function readSettingsFile (line 436) | function readSettingsFile(app: ConfigApp) {
  function writeSettingsFile (line 464) | function writeSettingsFile(app: ConfigApp, settings: any, cb: any) {
  function getSettingsFilename (line 474) | function getSettingsFilename(app: ConfigApp) {
  function isExternalSsl (line 486) | function isExternalSsl(config: Config): boolean {
  function getExternalHostname (line 496) | function getExternalHostname(config: Config) {
  function scanDefaults (line 512) | function scanDefaults(deltaEditor: DeltaEditor, vpath: string, item: any) {
  function convertOldDefaultsToDeltas (line 526) | function convertOldDefaultsToDeltas(

FILE: src/constants.ts
  constant SERVERROUTESPREFIX (line 1) | const SERVERROUTESPREFIX = '/skServer'

FILE: src/cors.ts
  function setupCors (line 6) | function setupCors(

FILE: src/debug.ts
  function createDebug (line 5) | function createDebug(debugName: string) {
  function listKnownDebugs (line 10) | function listKnownDebugs() {

FILE: src/deltaPriority.ts
  type SourcePriority (line 6) | interface SourcePriority {
  type SourcePrioritiesData (line 11) | interface SourcePrioritiesData {
  type PathValue (line 15) | interface PathValue {
  type TimestampedSource (line 20) | interface TimestampedSource {
  type SourcePrecedenceData (line 25) | interface SourcePrecedenceData {
  type PathLatestTimestamps (line 30) | type PathLatestTimestamps = Map<Path, TimestampedSource>
  type PathPrecedences (line 32) | type PathPrecedences = Map<SourceRef, SourcePrecedenceData>
  type ToPreferredDelta (line 54) | type ToPreferredDelta = (

FILE: src/deltacache.ts
  type StringKeyed (line 26) | interface StringKeyed {
  class DeltaCache (line 30) | class DeltaCache {
    method constructor (line 42) | constructor(app: SignalKServer, streambundle: StreamBundle) {
    method getContextAndPathParts (line 62) | getContextAndPathParts(msg: NormalizedDelta): string[] {
    method onValue (line 82) | onValue(msg: NormalizedDelta) {
    method setSourceDelta (line 110) | setSourceDelta(key: string, delta: any) {
    method deleteContext (line 115) | deleteContext(contextKey: string) {
    method pruneContexts (line 123) | pruneContexts(seconds: number) {
    method buildFull (line 134) | buildFull(user: string, path: string[]) {
    method getSources (line 154) | getSources() {
    method buildFullFromDeltas (line 163) | buildFullFromDeltas(
    method getCachedDeltas (line 186) | getCachedDeltas(contextFilter: ContextMatcher, user?: string, key?: st...
  function pathToProcessForFull (line 237) | function pathToProcessForFull(pathArray: any[]) {
  function pickDeltasFromBranch (line 244) | function pickDeltasFromBranch(acc: any[], obj: any) {
  function findDeltas (line 256) | function findDeltas(branchOrLeaf: any) {
  function ensureHasDollarSource (line 260) | function ensureHasDollarSource(normalizedDelta: NormalizedDelta): Source...
  function getLeafObject (line 269) | function getLeafObject(

FILE: src/deltachain.ts
  class DeltaChain (line 5) | class DeltaChain {
    method constructor (line 8) | constructor(private dispatchMessage: any) {
    method process (line 13) | process(msg: Delta) {
    method doProcess (line 17) | doProcess(index: number, msg: any) {
    method register (line 25) | register(handler: DeltaInputHandler) {
    method updateNexts (line 37) | updateNexts() {

FILE: src/deltaeditor.ts
  constant VALUES (line 22) | const VALUES = 'values'
  constant META (line 23) | const META = 'meta'
  constant SELF_VESSEL (line 24) | const SELF_VESSEL = 'vessels.self'
  class DeltaEditor (line 26) | class DeltaEditor {
    method constructor (line 29) | constructor() {
    method load (line 33) | load(filename: string) {
    method saveSync (line 43) | saveSync(filename: string) {
    method save (line 48) | save(filename: string): Promise<void> {
    method setValue (line 52) | setValue(context: string, path: string, value: any) {
    method setSelfValue (line 67) | setSelfValue(path: string, value: any) {
    method setMeta (line 71) | setMeta(context: string, path: string, value: any) {
    method getValue (line 75) | getValue(context: string, path: string) {
    method getSelfValue (line 85) | getSelfValue(path: string) {
    method getMeta (line 89) | getMeta(context: string, path: string) {
    method removeValue (line 94) | removeValue(context: string, path: string) {
    method removeSelfValue (line 115) | removeSelfValue(path: string) {
    method removeMeta (line 119) | removeMeta(context: string, path: string) {
  function setDelta (line 130) | function setDelta(
  function getDelta (line 163) | function getDelta(

FILE: src/deltastats.ts
  constant STATS_UPDATE_INTERVAL_SECONDS (line 21) | const STATS_UPDATE_INTERVAL_SECONDS = 5
  constant CONNECTION_WRITE_EVENT_NAME (line 22) | const CONNECTION_WRITE_EVENT_NAME = 'connectionwrite'
  type ConnectionWriteEvent (line 24) | interface ConnectionWriteEvent {
  class ProviderStats (line 29) | class ProviderStats {
    method constructor (line 36) | constructor() {
  type WithProviderStatistics (line 47) | interface WithProviderStatistics {
  function startDeltaStatistics (line 55) | function startDeltaStatistics(
  function incDeltaStatistics (line 93) | function incDeltaStatistics(
  function updateProviderPeriodStats (line 105) | function updateProviderPeriodStats(app: any) {

FILE: src/discovery.js
  function findUDPProvider (line 38) | function findUDPProvider(port) {
  function findTCPProvider (line 52) | function findTCPProvider(host, port) {
  function findWSProvider (line 67) | function findWSProvider(ip, wsType, host, port) {
  function discoverGoFree (line 84) | function discoverGoFree() {
  function discoverWLN10 (line 152) | function discoverWLN10() {
  function discoverSignalkWs (line 197) | function discoverSignalkWs(wsType) {
  function isLocalIP (line 260) | function isLocalIP(IP) {

FILE: src/events.ts
  type EventsSpark (line 7) | interface EventsSpark {
  function startEvents (line 13) | function startEvents(
  function startServerEvents (line 33) | function startServerEvents(
  type Handler (line 82) | type Handler = (...args: any[]) => void
  type EventMap (line 83) | interface EventMap {
  function safeApply (line 87) | function safeApply<T, A extends any[]>(
  function arrayClone (line 102) | function arrayClone<T>(arr: T[]): T[] {
  type EventName (line 111) | type EventName = Brand<string, 'eventname'>
  type EmitterId (line 112) | type EmitterId = Brand<string, 'emitterId'>
  type ListenerId (line 113) | type ListenerId = Brand<string, 'listenerid'>
  type EventsActorId (line 114) | type EventsActorId = EmitterId & ListenerId
  type WrappedEmitter (line 116) | interface WrappedEmitter {
  type WithWrappedEmitter (line 144) | interface WithWrappedEmitter {
  function wrapEmitter (line 148) | function wrapEmitter(targetEmitter: EventEmitter): WrappedEmitter {

FILE: src/index.ts
  class Server (line 72) | class Server {
    method constructor (line 85) | constructor(opts: { securityConfig: SecurityConfig }) {
    method start (line 392) | start() {
    method reload (line 527) | reload(mixed: any) {
    method stop (line 556) | async stop(cb?: () => void) {
  function identifyPluginFromStack (line 613) | function identifyPluginFromStack(
  function installProcessErrorHandlers (line 625) | function installProcessErrorHandlers(app: any) {
  function createServer (line 648) | function createServer(app: any, cb: (err: any, server?: any) => void) {
  function startRedirectToSsl (line 671) | function startRedirectToSsl(
  function startMdns (line 688) | function startMdns(app: ServerApp & WithConfig) {
  function startInterfaces (line 701) | async function startInterfaces(
  function isValidUpdate (line 794) | function isValidUpdate(update: unknown): update is Partial<Update> {
  function filterStaticSelfData (line 816) | function filterStaticSelfData(delta: any, selfContext: string) {
  function filterSelfDataKP (line 837) | function filterSelfDataKP(pathValue: any) {

FILE: src/interfaces/applicationData.js
  function withFileLock (line 30) | function withFileLock(filePath, fn) {
  constant DANGEROUS_PATH_SEGMENTS (line 46) | const DANGEROUS_PATH_SEGMENTS = ['__proto__', 'constructor', 'prototype']
  function isPrototypePollutionPath (line 48) | function isPrototypePollutionPath(pathString) {
  function hasPrototypePollutionPatch (line 53) | function hasPrototypePollutionPatch(patches) {
  function listVersions (line 118) | function listVersions(req, res, isUser) {
  function getApplicationData (line 136) | function getApplicationData(req, res, isUser) {
  function postApplicationData (line 174) | async function postApplicationData(req, res, isUser) {
  function readApplicationData (line 227) | function readApplicationData(req, appid, version, isUser) {
  function validateAppId (line 251) | function validateAppId(appid) {
  function validateVersion (line 259) | function validateVersion(version) {
  function dirForApplicationData (line 263) | function dirForApplicationData(req, appid, isUser) {
  function pathForApplicationData (line 273) | function pathForApplicationData(req, appid, version, isUser) {
  function saveApplicationData (line 287) | async function saveApplicationData(req, appid, version, isUser, data) {

FILE: src/interfaces/appstore.js
  function findPluginsAndWebapps (line 186) | function findPluginsAndWebapps() {
  function getInstalledPackageNames (line 205) | function getInstalledPackageNames() {
  function getPlugin (line 218) | function getPlugin(id) {
  function getWebApp (line 222) | function getWebApp(id) {
  function emptyAppStoreInfo (line 231) | function emptyAppStoreInfo(storeAvailable = true) {
  function getAllModuleInfo (line 243) | function getAllModuleInfo(plugins, webapps, serverVersion, distTagsMap =...
  function getModulesInfo (line 297) | function getModulesInfo(modules, existing, result, distTagsMap) {
  function addIfNotDuplicate (line 395) | function addIfNotDuplicate(theArray, moduleInfo) {
  function getNpmUrl (line 401) | function getNpmUrl(moduleInfo) {
  function sendAppStoreChangedEvent (line 406) | function sendAppStoreChangedEvent() {
  function installSKModule (line 422) | function installSKModule(module, version) {
  function removeSKModule (line 441) | function removeSKModule(module, deleteData) {
  function updateSKModule (line 447) | function updateSKModule(module, version, isRemove, pluginId, deleteData) {
  function packageNameIs (line 505) | function packageNameIs(name) {

FILE: src/interfaces/logfiles.js
  function mountApi (line 31) | function mountApi(app) {

FILE: src/interfaces/mfd_webapp.ts
  constant PUBLISH_PORT (line 7) | const PUBLISH_PORT = 2053
  constant MULTICAST_GROUP_IP (line 8) | const MULTICAST_GROUP_IP = '239.2.1.1'
  method start (line 18) | start() {

FILE: src/interfaces/nmea-tcp.ts
  constant BUFFER_LIMIT (line 22) | const BUFFER_LIMIT = process.env.BACKPRESSURE_ENTER
  constant MAX_BUFFER (line 25) | const MAX_BUFFER = process.env.MAXSENDBUFFERSIZE
  constant MAX_BUFFER_TIME (line 28) | const MAX_BUFFER_TIME = process.env.MAXSENDBUFFERCHECKTIME
  type SocketWithId (line 32) | interface SocketWithId extends Socket {
  type NmeaTcpApp (line 37) | interface NmeaTcpApp extends SignalKServer {

FILE: src/interfaces/playground.js
  function detectType (line 66) | function detectType(message) {

FILE: src/interfaces/plugins.ts
  constant DEFAULT_ENABLED_PLUGINS (line 81) | const DEFAULT_ENABLED_PLUGINS = process.env.DEFAULTENABLEDPLUGINS
  type PluginId (line 85) | type PluginId = Brand<string, 'PluginId'>
  type PluginManager (line 86) | interface PluginManager {
  type PluginInfo (line 92) | interface PluginInfo extends Plugin {
  function backwardsCompat (line 106) | function backwardsCompat(url: string) {
  method start (line 113) | async start() {
  function getPluginResponseInfos (line 139) | function getPluginResponseInfos() {
  function getPluginsList (line 152) | function getPluginsList(enabled?: boolean) {
  function getPluginResponseInfo (line 173) | function getPluginResponseInfo(plugin: PluginInfo, providerStatus: any) {
  function ensureExists (line 246) | function ensureExists(dir: string) {
  function pathForPluginId (line 252) | function pathForPluginId(id: string) {
  function dirForPluginId (line 256) | function dirForPluginId(id: string) {
  function savePluginOptions (line 262) | function savePluginOptions(
  function getPluginOptions (line 278) | function getPluginOptions(id: string) {
  function startPlugins (line 309) | async function startPlugins(app: any) {
  function handleMessageWrapper (line 344) | function handleMessageWrapper(app: any, id: string) {
  function getSelfPath (line 364) | function getSelfPath(aPath: string) {
  function getPath (line 368) | function getPath(aPath: string) {
  function putSelfPath (line 379) | function putSelfPath(
  function putPath (line 396) | function putPath(
  function getSerialPorts (line 422) | function getSerialPorts() {
  function registerPlugin (line 426) | async function registerPlugin(
  function stopPlugin (line 495) | function stopPlugin(plugin: PluginInfo): Promise<any> {
  function setPluginStartedMessage (line 513) | function setPluginStartedMessage(plugin: PluginInfo) {
  function doPluginStart (line 527) | function doPluginStart(
  function doRegisterPlugin (line 564) | async function doRegisterPlugin(

FILE: src/interfaces/providers.ts
  constant MAX_CANBUS_UNIQUE_NUMBER (line 24) | const MAX_CANBUS_UNIQUE_NUMBER = 0x1fffff
  type PipeElement (line 26) | interface PipeElement {
  type PipedProvider (line 36) | interface PipedProvider {
  type ProviderRequest (line 42) | interface ProviderRequest {
  type ProviderResponse (line 58) | interface ProviderResponse {
  type App (line 71) | interface App extends ConfigApp, IRouter {
  function getProviders (line 75) | function getProviders(
  function applyProviderSettings (line 109) | function applyProviderSettings(
  function isValidProviderBody (line 135) | function isValidProviderBody(body: unknown): body is ProviderRequest {
  function updateProvider (line 218) | function updateProvider(

FILE: src/interfaces/rest.js
  function enhanceMetadataResponse (line 28) | function enhanceMetadataResponse(metadata, signalkPath, username) {
  constant SKIP_KEYS (line 64) | const SKIP_KEYS = new Set([
  function enhanceTreeMetadata (line 74) | function enhanceTreeMetadata(node, pathParts, username) {
  function collectMeta (line 89) | function collectMeta(node, pathParts, result) {
  function snapshotHandler (line 119) | function snapshotHandler(snapshotPath, req, res, next) {
  function sendResult (line 239) | function sendResult(last, aPath) {

FILE: src/interfaces/tcp.ts
  type SocketWithId (line 30) | interface SocketWithId extends Socket {
  method id (line 63) | get id() {
  method getBufferLength (line 66) | getBufferLength() {
  method write (line 69) | write(delta) {
  method destroy (line 74) | destroy() {
  function socketMessageHandler (line 151) | function socketMessageHandler(

FILE: src/interfaces/unitpreferences-api.js
  function validateFormula (line 15) | function validateFormula(formula) {
  constant VALID_PRESET_NAME (line 24) | const VALID_PRESET_NAME = /^[a-zA-Z0-9_-]+$/
  function validateDefinitions (line 31) | function validateDefinitions(definitions) {
  function validatePreset (line 59) | function validatePreset(preset, definitions) {
  constant PACKAGE_UNITPREFS_DIR (line 144) | const PACKAGE_UNITPREFS_DIR = path.join(__dirname, '../../unitpreferences')
  function checkPresetExists (line 149) | function checkPresetExists(presetName) {

FILE: src/interfaces/webapps.js
  function mountWebModules (line 59) | function mountWebModules(app, keyword) {
  function mountApis (line 73) | function mountApis(app) {

FILE: src/interfaces/ws.ts
  type SkPrincipal (line 59) | interface SkPrincipal {
  type SignalKSparkRequest (line 63) | interface SignalKSparkRequest {
  type Spark (line 76) | interface Spark {
  type WsMessage (line 104) | interface WsMessage {
  type WsRequestReply (line 120) | interface WsRequestReply extends UpdateOptions {
  function isWsRequestReply (line 125) | function isWsRequestReply(msg: unknown): msg is WsRequestReply {
  function normalizeStatusCode (line 139) | function normalizeStatusCode(statusCode: unknown): number | null {
  function normalizeMessage (line 145) | function normalizeMessage(message: unknown): string | null {
  function normalizePercentComplete (line 149) | function normalizePercentComplete(percentComplete: unknown): number | nu...
  type PathSources (line 158) | interface PathSources {
  type SecurityStrategy (line 164) | interface SecurityStrategy {
  type SubscriptionManager (line 186) | interface SubscriptionManager {
  type HistoryProvider (line 197) | interface HistoryProvider {
  type HistoryOptions (line 209) | interface HistoryOptions {
  type WithContext (line 215) | interface WithContext {
  type DeltaCache (line 219) | interface DeltaCache {
  type WsAppConfig (line 226) | interface WsAppConfig {
  type WsApp (line 238) | interface WsApp {
  type WsApi (line 264) | interface WsApi {
  constant DEFAULT_MAX_WS_CONNECTIONS_PER_IP (line 283) | const DEFAULT_MAX_WS_CONNECTIONS_PER_IP = 30
  function wsInterface (line 285) | function wsInterface(app: WsApp): WsApi {
  function createPrimusAuthorize (line 852) | function createPrimusAuthorize(
  function processUpdates (line 921) | function processUpdates(
  type MetaHandlerContext (line 992) | interface MetaHandlerContext {
  function handleValuesMeta (line 998) | function handleValuesMeta(
  function handleUpdatesMeta (line 1070) | function handleUpdatesMeta(
  function sendMetaData (line 1080) | function sendMetaData(app: WsApp, spark: Spark, delta: Delta): void {
  function processSubscribe (line 1090) | function processSubscribe(
  function processUnsubscribe (line 1122) | function processUnsubscribe(
  function wrapWithVerifyWS (line 1154) | function wrapWithVerifyWS(
  function sendHello (line 1187) | function sendHello(
  function handlePlaybackConnection (line 1198) | function handlePlaybackConnection(
  function handleRealtimeConnection (line 1234) | function handleRealtimeConnection(
  function sendLatestDeltas (line 1335) | function sendLatestDeltas(
  function startServerLog (line 1356) | function startServerLog(app: WsApp, spark: Spark): () => void {

FILE: src/logging.js
  function storeOutput (line 28) | function storeOutput(output, isError) {
  function enableDebug (line 64) | function enableDebug(enabled) {

FILE: src/login-rate-limiter.ts
  constant LOGIN_RATE_LIMIT_MESSAGE (line 1) | const LOGIN_RATE_LIMIT_MESSAGE =
  type LoginRateLimiter (line 4) | interface LoginRateLimiter {
  type Entry (line 9) | interface Entry {
  function createLoginRateLimiter (line 14) | function createLoginRateLimiter(

FILE: src/modules.ts
  type ModuleData (line 30) | interface ModuleData {
  type NpmDistTags (line 36) | interface NpmDistTags {
  type WasmCapabilities (line 41) | interface WasmCapabilities {
  type NpmPackageData (line 55) | interface NpmPackageData {
  type NpmSearchResponse (line 69) | interface NpmSearchResponse {
  type NpmModuleData (line 74) | interface NpmModuleData {
  type Package (line 78) | interface Package {
  function findModulesInDir (line 89) | function findModulesInDir(dir: string, keyword: string): ModuleData[] {
  function getModulePaths (line 134) | function getModulePaths(config: Config) {
  function modulesWithKeyword (line 150) | function modulesWithKeyword(config: Config, keyword: string) {
  function installModule (line 161) | function installModule(
  function removeModule (line 172) | function removeModule(
  function cleanupAfterRemove (line 188) | function cleanupAfterRemove(
  function getPluginDataSize (line 250) | async function getPluginDataSize(
  function restoreModules (line 307) | function restoreModules(
  function runNpm (line 316) | function runNpm(
  function isTheServerModule (line 380) | function isTheServerModule(moduleName: string, config: Config) {
  function findModulesWithKeyword (line 389) | async function findModulesWithKeyword(
  function searchByKeyword (line 423) | async function searchByKeyword(keyword: string): Promise<NpmModuleData[]> {
  function fetchDistTagsForPackages (line 454) | async function fetchDistTagsForPackages(
  function doFetchDistTags (line 489) | function doFetchDistTags() {
  function getLatestServerVersion (line 493) | async function getLatestServerVersion(
  function checkForNewServerVersion (line 517) | function checkForNewServerVersion(
  function getAuthor (line 538) | function getAuthor(thePackage: Package): string {
  function getKeywords (line 544) | function getKeywords(thePackage: NpmPackageData): string[] {
  function importOrRequire (line 550) | async function importOrRequire(moduleDir: string) {

FILE: src/oidc/authorization.ts
  function buildAuthorizationUrl (line 27) | function buildAuthorizationUrl(

FILE: src/oidc/config.ts
  function parseEnvConfig (line 27) | function parseEnvConfig(): PartialOIDCConfig {
  function mergeConfigs (line 109) | function mergeConfigs(
  function validateOIDCConfig (line 150) | function validateOIDCConfig(
  function parseOIDCConfig (line 249) | function parseOIDCConfig(securityConfig: {
  function isOIDCEnabled (line 264) | function isOIDCEnabled(config: OIDCConfig): boolean {

FILE: src/oidc/discovery.ts
  constant CACHE_TTL_MS (line 19) | const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour
  type FetchFn (line 25) | type FetchFn = typeof fetch
  function setFetchFunction (line 31) | function setFetchFunction(fn: FetchFn): void {
  function resetFetchFunction (line 38) | function resetFetchFunction(): void {
  function fetchDiscoveryDocument (line 48) | async function fetchDiscoveryDocument(
  function getDiscoveryDocument (line 103) | async function getDiscoveryDocument(
  function clearDiscoveryCache (line 128) | function clearDiscoveryCache(): void {

FILE: src/oidc/id-token-validation.ts
  type JoseModule (line 24) | type JoseModule = typeof import('jose')
  function getJose (line 27) | async function getJose(): Promise<JoseModule> {
  type JSONWebKeySet (line 37) | interface JSONWebKeySet {
  type ValidatedIdTokenClaims (line 50) | interface ValidatedIdTokenClaims {
  type FetchFn (line 65) | type FetchFn = typeof fetch
  function setFetchFunction (line 71) | function setFetchFunction(fn: FetchFn): void {
  function resetFetchFunction (line 78) | function resetFetchFunction(): void {
  type JwksCache (line 83) | interface JwksCache {
  constant JWKS_CACHE_TTL_MS (line 89) | const JWKS_CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour
  function clearJwksCache (line 94) | function clearJwksCache(): void {
  function clearJwksCacheForUri (line 101) | function clearJwksCacheForUri(jwksUri: string): void {
  function fetchJwks (line 110) | async function fetchJwks(
  function isSignatureError (line 160) | function isSignatureError(err: unknown): boolean {
  function validateIdToken (line 199) | async function validateIdToken(

FILE: src/oidc/oidc-admin.ts
  constant SERVERROUTESPREFIX (line 22) | const SERVERROUTESPREFIX = '/skServer'
  type SecurityConfigForOIDC (line 28) | interface SecurityConfigForOIDC {
  type OIDCAdminDependencies (line 36) | interface OIDCAdminDependencies {
  type OIDCAdminResponse (line 54) | interface OIDCAdminResponse {
  function buildOIDCAdminResponse (line 76) | function buildOIDCAdminResponse(
  function checkAllowConfigure (line 132) | function checkAllowConfigure(
  function parseGroupsIfString (line 147) | function parseGroupsIfString(groups: unknown): string[] | undefined {
  function registerOIDCAdminRoutes (line 164) | function registerOIDCAdminRoutes(

FILE: src/oidc/oidc-auth.ts
  function arraysEqualIgnoringOrder (line 48) | function arraysEqualIgnoringOrder(
  function validateAndMergeUserinfoClaims (line 73) | function validateAndMergeUserinfoClaims(
  type OIDCAuthDependencies (line 101) | interface OIDCAuthDependencies {
  function isSafeRelativeUrl (line 132) | function isSafeRelativeUrl(url: unknown): url is string {
  function findOrCreateOIDCUser (line 159) | async function findOrCreateOIDCUser(
  function registerOIDCRoutes (line 262) | function registerOIDCRoutes(

FILE: src/oidc/permission-mapping.ts
  function hasIntersection (line 22) | function hasIntersection(arr1: string[], arr2: string[]): boolean {
  function mapGroupsToPermission (line 44) | function mapGroupsToPermission(

FILE: src/oidc/pkce.ts
  function generateCodeVerifier (line 25) | function generateCodeVerifier(): string {
  function calculateCodeChallenge (line 37) | function calculateCodeChallenge(verifier: string): string {

FILE: src/oidc/state.ts
  constant ALGORITHM (line 26) | const ALGORITHM = 'aes-256-gcm'
  constant IV_LENGTH (line 27) | const IV_LENGTH = 16
  constant AUTH_TAG_LENGTH (line 28) | const AUTH_TAG_LENGTH = 16
  function generateState (line 34) | function generateState(): string {
  function generateNonce (line 42) | function generateNonce(): string {
  function createAuthState (line 49) | function createAuthState(
  function validateState (line 67) | function validateState(
  function deriveKey (line 90) | function deriveKey(secretKey: string): Buffer {
  function encryptState (line 98) | function encryptState(state: OIDCAuthState, secretKey: string): string {
  function decryptState (line 118) | function decryptState(

FILE: src/oidc/token-exchange.ts
  type FetchFn (line 26) | type FetchFn = typeof fetch
  function setFetchFunction (line 32) | function setFetchFunction(fn: FetchFn): void {
  function resetFetchFunction (line 39) | function resetFetchFunction(): void {
  function exchangeAuthorizationCode (line 52) | async function exchangeAuthorizationCode(
  function fetchUserinfo (line 138) | async function fetchUserinfo(

FILE: src/oidc/types.ts
  type SignalKPermission (line 20) | type SignalKPermission = 'readonly' | 'readwrite' | 'admin'
  type OIDCConfig (line 25) | interface OIDCConfig {
  type PartialOIDCConfig (line 60) | interface PartialOIDCConfig {
  type OIDCAuthState (line 79) | interface OIDCAuthState {
  type OIDCTokens (line 91) | interface OIDCTokens {
  type OIDCUserInfo (line 102) | interface OIDCUserInfo {
  type OIDCUserIdentifier (line 113) | interface OIDCUserIdentifier {
  type DiscoveryCache (line 121) | interface DiscoveryCache {
  type OIDCProviderMetadata (line 130) | interface OIDCProviderMetadata {
  type OIDCErrorCode (line 145) | type OIDCErrorCode =
  class OIDCError (line 160) | class OIDCError extends Error {
    method constructor (line 161) | constructor(
  constant OIDC_DEFAULTS (line 175) | const OIDC_DEFAULTS: Omit<
  constant STATE_COOKIE_NAME (line 190) | const STATE_COOKIE_NAME = 'OIDC_STATE'
  constant STATE_MAX_AGE_MS (line 191) | const STATE_MAX_AGE_MS = 10 * 60 * 1000 // 10 minutes
  type OIDCCryptoService (line 200) | interface OIDCCryptoService {
  type ProviderUserLookup (line 213) | interface ProviderUserLookup {
  type ExternalUserService (line 228) | interface ExternalUserService {
  type ExternalUser (line 249) | interface ExternalUser {

FILE: src/oidc/user-info.ts
  function decodeIdToken (line 25) | function decodeIdToken(idToken: string): Record<string, unknown> {
  function extractUserInfo (line 53) | function extractUserInfo(idToken: string): OIDCUserInfo {

FILE: src/pipedproviders.ts
  type PipeElementConfig (line 24) | interface PipeElementConfig {
  type PipedProviderConfig (line 37) | interface PipedProviderConfig {
  class PipedProvider (line 43) | class PipedProvider {}
  function pipedProviders (line 45) | function pipedProviders(

FILE: src/plugin-paths.ts
  constant PLUGIN_CONFIG_DATA_DIR (line 3) | const PLUGIN_CONFIG_DATA_DIR = 'plugin-config-data'
  function pluginConfigPath (line 5) | function pluginConfigPath(configPath: string, pluginId: string): string {
  function pluginDataDir (line 9) | function pluginDataDir(configPath: string, pluginId: string): string {

FILE: src/pluginid.ts
  function derivePluginId (line 18) | function derivePluginId(packageName: string): string {

FILE: src/ports.ts
  constant SD_LISTEN_FDS_START (line 19) | const SD_LISTEN_FDS_START = 3
  function getPrimaryPort (line 27) | function getPrimaryPort(app: WithConfig) {
  function getSecondaryPort (line 37) | function getSecondaryPort(app: WithConfig): any {
  function getExternalPort (line 49) | function getExternalPort(app: WithConfig) {

FILE: src/put.ts
  type WsInterface (line 19) | interface WsInterface {
  type PutAppInterfaces (line 30) | interface PutAppInterfaces {
  type PutApp (line 35) | interface PutApp extends Application {
  type PathApp (line 46) | interface PathApp {
  type NotificationApp (line 52) | interface NotificationApp {
  type ActionCallback (line 61) | type ActionCallback = (reply: ActionResult) => void
  type ActionResult (line 63) | interface ActionResult {
  type ActionHandler (line 69) | type ActionHandler = (
  type DeleteHandler (line 76) | type DeleteHandler = (
  type ActionHandlers (line 82) | interface ActionHandlers {
  type PutBody (line 90) | interface PutBody {
  type SkRequest (line 95) | interface SkRequest extends Request {
  function start (line 110) | function start(app: PutApp): void {
  function deletePath (line 388) | function deletePath(
  function putPath (line 482) | function putPath(
  function registerActionHandler (line 628) | function registerActionHandler(
  function deRegisterActionHandler (line 649) | function deRegisterActionHandler(
  function putNotification (line 664) | function putNotification(

FILE: src/requestResponse.ts
  type RequestState (line 6) | type RequestState = 'PENDING' | 'COMPLETED'
  type RequestType (line 7) | type RequestType = 'put' | 'delete' | 'accessRequest'
  type ClientRequest (line 9) | interface ClientRequest {
  type Request (line 14) | interface Request {
  type AccessRequestData (line 35) | interface AccessRequestData {
  type Reply (line 40) | interface Reply {
  type UpdateOptions (line 53) | interface UpdateOptions {
  type AppWithIntervals (line 60) | interface AppWithIntervals {
  function resetRequests (line 70) | function resetRequests(): void {
  function createRequest (line 76) | function createRequest(
  function createReply (line 111) | function createReply(request: Request): Reply {
  function updateRequest (line 132) | function updateRequest(
  function queryRequest (line 174) | function queryRequest(requestId: string): Promise<Reply> {
  function findRequest (line 187) | function findRequest(
  function filterRequests (line 193) | function filterRequests(
  function pruneRequests (line 202) | function pruneRequests(): void {

FILE: src/security.ts
  type WithSecurityStrategy (line 40) | interface WithSecurityStrategy {
  type LoginStatusResponse (line 44) | interface LoginStatusResponse {
  type ACL (line 54) | interface ACL {
  type OIDCUserIdentifier (line 65) | interface OIDCUserIdentifier {
  function isOIDCUserIdentifier (line 76) | function isOIDCUserIdentifier(
  type User (line 87) | interface User {
  type UserData (line 93) | interface UserData {
  type UserDataUpdate (line 97) | interface UserDataUpdate {
  type UserWithPassword (line 102) | interface UserWithPassword {
  type Device (line 108) | interface Device {
  type DeviceDataUpdate (line 117) | interface DeviceDataUpdate {
  type OIDCSecurityConfig (line 122) | interface OIDCSecurityConfig {
  type SecurityConfig (line 133) | interface SecurityConfig {
  type RequestStatusData (line 147) | interface RequestStatusData {
  type SecurityStrategy (line 153) | interface SecurityStrategy {
  class InvalidTokenError (line 250) | class InvalidTokenError extends Error {
    method constructor (line 251) | constructor(...args: any[]) {
  function startSecurity (line 257) | function startSecurity(
  function getSecurityConfig (line 290) | function getSecurityConfig(
  function pathForSecurityConfig (line 313) | function pathForSecurityConfig(app: WithConfig) {
  function saveSecurityConfig (line 317) | function saveSecurityConfig(
  function getCertificateOptions (line 344) | function getCertificateOptions(app: WithConfig, cb: any) {
  function hasStrictPermissions (line 391) | function hasStrictPermissions(stat: Stats) {
  function getCAChainArray (line 399) | function getCAChainArray(filename: string) {
  function createCertificateOptions (line 413) | function createCertificateOptions(
  function requestAccess (line 437) | function requestAccess(
  type SecurityConfigSaver (line 447) | type SecurityConfigSaver = (
  type SecurityConfigGetter (line 452) | type SecurityConfigGetter = (app: any) => any
  function getRateLimitValidationOptions (line 460) | function getRateLimitValidationOptions(app: WithConfig) {

FILE: src/serialports.ts
  function listSerialPorts (line 35) | function listSerialPorts() {
  function listSafeSerialPortsDevSerialById (line 44) | function listSafeSerialPortsDevSerialById() {
  function listSafeSerialPortsDevSerialByPath (line 53) | function listSafeSerialPortsDevSerialByPath() {
  function listSafeSerialPortsOpenPlotter (line 62) | function listSafeSerialPortsOpenPlotter() {

FILE: src/serverroutes.ts
  type HttpRateLimitOverrides (line 74) | type HttpRateLimitOverrides = {
  constant DEFAULT_HTTP_RATE_LIMIT_WINDOW_MS (line 80) | const DEFAULT_HTTP_RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000 // 10 minutes
  constant DEFAULT_HTTP_RATE_LIMIT_API_MAX (line 81) | const DEFAULT_HTTP_RATE_LIMIT_API_MAX = 1000
  constant DEFAULT_HTTP_RATE_LIMIT_LOGIN_STATUS_MAX (line 82) | const DEFAULT_HTTP_RATE_LIMIT_LOGIN_STATUS_MAX = 1000
  function getHttpRateLimitOverridesFromEnv (line 84) | function getHttpRateLimitOverridesFromEnv(): HttpRateLimitOverrides {
  type ScriptsApp (line 127) | interface ScriptsApp {
  type App (line 133) | interface App
  type ModuleInfo (line 152) | interface ModuleInfo {
  function serveIndexWithAddonScripts (line 264) | function serveIndexWithAddonScripts(indexPath: string, res: Response) {
  function getConfigSavingCallback (line 415) | function getConfigSavingCallback(success: any, failure: any, res: Respon...
  function checkAllowConfigure (line 436) | function checkAllowConfigure(req: Request, res: Response) {
  function addUser (line 845) | function addUser(
  function writeOldDefaults (line 1063) | function writeOldDefaults(req: Request, res: Response) {
  function makeNumber (line 1170) | function makeNumber(num: string) {
  function listSafeRestoreFiles (line 1360) | function listSafeRestoreFiles(restorePath: string): Promise<string[]> {
  function sendRestoreStatus (line 1382) | function sendRestoreStatus(
  function makeRemoteRequest (line 1770) | function makeRemoteRequest(
  function getCookie (line 1819) | function getCookie(req: Request, name: string): string | undefined {

FILE: src/serverstate/store.ts
  constant SERVERSTATEDIRNAME (line 7) | const SERVERSTATEDIRNAME = 'serverState'
  class Store (line 9) | class Store {
    method constructor (line 14) | constructor(
    method waitForInit (line 34) | async waitForInit(): Promise<void> {
    method read (line 41) | async read(): Promise<any> {
    method write (line 48) | async write(data: any) {
    method init (line 56) | private async init() {

FILE: src/streambundle.ts
  class StreamBundle (line 28) | class StreamBundle implements IStreamBundle {
    method constructor (line 41) | constructor(selfId: string) {
    method pushDelta (line 55) | pushDelta(delta: Delta) {
    method push (line 94) | push(path: Path, normalizedDelta: NormalizedDelta) {
    method getMetaBus (line 118) | getMetaBus() {
    method getSelfMetaBus (line 122) | getSelfMetaBus() {
    method getBus (line 126) | getBus(path?: Path) {
    method getSelfStream (line 139) | getSelfStream(path?: Path) {
    method getSelfBus (line 151) | getSelfBus(path?: Path) {
    method getAvailablePaths (line 163) | getAvailablePaths() {
  function toDelta (line 168) | function toDelta(normalizedDeltaData: NormalizedDelta): Delta {

FILE: src/subscriptionmanager.ts
  type BusesMap (line 39) | interface BusesMap {
  class SubscriptionManager (line 43) | class SubscriptionManager implements ISubscriptionManager {
    method constructor (line 47) | constructor(app: any) {
    method subscribe (line 53) | subscribe(
    method unsubscribe (line 149) | unsubscribe(msg: UnsubscribeMessage, unsubscribes: Unsubscribes) {
  function handleSubscribeRows (line 171) | function handleSubscribeRows(
  type App (line 198) | interface App {
  function handleSubscribeRow (line 202) | function handleSubscribeRow(
  function pathMatcher (line 281) | function pathMatcher(path: string = '*') {
  function contextMatcher (line 290) | function contextMatcher(
  function checkPosition (line 331) | function checkPosition(

FILE: src/tokensecurity.ts
  constant CONFIG_PLUGINID (line 83) | const CONFIG_PLUGINID = 'sk-simple-token-security-config'
  constant BROWSER_LOGININFO_COOKIE_NAME (line 92) | const BROWSER_LOGININFO_COOKIE_NAME = 'skLoginInfo'
  constant LOGIN_FAILED_MESSAGE (line 94) | const LOGIN_FAILED_MESSAGE = 'Invalid username/password'
  constant VALID_PERMISSIONS (line 95) | const VALID_PERMISSIONS = new Set(['readonly', 'readwrite', 'admin'])
  constant DUMMY_HASH (line 98) | const DUMMY_HASH = '$2b$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVW...
  function isNever (line 100) | function isNever(expiration: string | undefined): boolean {
  type SKRequest (line 107) | interface SKRequest extends Request {
  type Principal (line 119) | interface Principal {
  type JWTPayload (line 124) | interface JWTPayload {
  type LoginResponse (line 132) | interface LoginResponse {
  type CookieOptions (line 140) | interface CookieOptions {
  type TokenSecurityOptions (line 150) | interface TokenSecurityOptions {
  type AccessRequest (line 164) | interface AccessRequest {
  type TokenSecurityApp (line 190) | interface TokenSecurityApp
  type TokenSecurityStrategy (line 196) | interface TokenSecurityStrategy extends SecurityStrategy {
  type WSConnection (line 220) | interface WSConnection {
  function tokenSecurityFactory (line 230) | function tokenSecurityFactory(

FILE: src/types.ts
  type HelloMessage (line 6) | interface HelloMessage {
  type ICallback (line 14) | type ICallback<T> = (error?: Error | null, result?: T) => void
  type SignalKServer (line 16) | interface SignalKServer extends ServerAPI {
  class Interface (line 25) | class Interface {
  type MdnsAdvertisement (line 31) | interface MdnsAdvertisement {
  type ContextMatcher (line 37) | type ContextMatcher = (_: WithContext) => boolean

FILE: src/unitpreferences/loader.ts
  constant PACKAGE_UNITPREFS_DIR (line 16) | const PACKAGE_UNITPREFS_DIR = path.join(__dirname, '../../unitpreferences')
  constant DEFAULT_PRESET (line 17) | const DEFAULT_PRESET = 'nautical-metric'
  constant VALID_USERNAME (line 18) | const VALID_USERNAME = /^[a-zA-Z0-9_.\-@]+$/
  constant USER_PREFS_FILE (line 19) | const USER_PREFS_FILE = '1.0.0.json'
  function validateUsername (line 35) | function validateUsername(username: string): void {
  function getUserPrefsPath (line 41) | function getUserPrefsPath(username: string): string {
  function setApplicationDataPath (line 58) | function setApplicationDataPath(configPath: string): void {
  function getConfigUnitprefsDir (line 64) | function getConfigUnitprefsDir(): string {
  function ensureConfigDir (line 68) | function ensureConfigDir(): void {
  function loadAll (line 107) | function loadAll(): void {
  function loadActivePreset (line 192) | function loadActivePreset(): void {
  function getCategories (line 228) | function getCategories(): CategoryMap {
  function getCustomCategories (line 237) | function getCustomCategories(): { [category: string]: string } {
  function getStandardDefinitions (line 240) | function getStandardDefinitions(): UnitDefinitions {
  function getCustomDefinitions (line 243) | function getCustomDefinitions(): UnitDefinitions {
  function getActivePreset (line 246) | function getActivePreset(): Preset {
  function getConfig (line 249) | function getConfig(): UnitPreferencesConfig {
  function reloadPreset (line 253) | function reloadPreset(): void {
  function reloadCustomDefinitions (line 264) | function reloadCustomDefinitions(): void {
  function reloadCustomCategories (line 275) | function reloadCustomCategories(): void {
  function getMergedDefinitions (line 287) | function getMergedDefinitions(): UnitDefinitions {
  function getDefaultCategory (line 303) | function getDefaultCategory(
  function getBaseUnitToCategories (line 336) | function getBaseUnitToCategories(): { [baseUnit: string]: string[] } {
  function getCategoryForBaseUnit (line 356) | function getCategoryForBaseUnit(
  function loadUserPreferences (line 381) | function loadUserPreferences(
  function saveUserPreferences (line 410) | function saveUserPreferences(
  function loadPresetByName (line 422) | function loadPresetByName(presetName: string): Preset | null {
  function getActivePresetForUser (line 452) | function getActivePresetForUser(username?: string): Preset {

FILE: src/unitpreferences/resolver.ts
  function resolveDisplayUnits (line 17) | function resolveDisplayUnits(
  function validateCategoryAssignment (line 152) | function validateCategoryAssignment(

FILE: src/unitpreferences/types.ts
  type PrimaryCategoryMap (line 2) | interface PrimaryCategoryMap {
  type UserUnitPreferences (line 7) | interface UserUnitPreferences {
  type CategoryMap (line 13) | interface CategoryMap {
  type ConversionFormula (line 21) | interface ConversionFormula {
  type UnitDefinitions (line 30) | interface UnitDefinitions {
  type Preset (line 40) | interface Preset {
  type UnitPreferencesConfig (line 55) | interface UnitPreferencesConfig {
  type DisplayUnitsMetadata (line 62) | interface DisplayUnitsMetadata {
  type EnhancedDisplayUnits (line 72) | interface EnhancedDisplayUnits {

FILE: src/version.ts
  function checkNodeVersion (line 6) | function checkNodeVersion() {

FILE: src/wasm/bindings/binary-stream.ts
  function createBinaryDataReader (line 16) | function createBinaryDataReader(memoryRef: {
  function createBinaryStreamBinding (line 40) | function createBinaryStreamBinding(

FILE: src/wasm/bindings/env-imports.ts
  type EnvImportsOptions (line 32) | interface EnvImportsOptions {
  function createUtf8Reader (line 46) | function createUtf8Reader(memoryRef: {
  function createEnvImports (line 62) | function createEnvImports(

FILE: src/wasm/bindings/radar-provider.ts
  function callWasmRadarHandler (line 24) | async function callWasmRadarHandler(
  function updateRadarProviderInstance (line 170) | function updateRadarProviderInstance(
  function cleanupRadarProviders (line 186) | function cleanupRadarProviders(pluginId: string, app?: any): void {
  function createRadarProviderBinding (line 215) | function createRadarProviderBinding(
  function createRadarEmitSpokesBinding (line 894) | function createRadarEmitSpokesBinding(

FILE: src/wasm/bindings/resource-provider.ts
  function callWasmResourceHandler (line 24) | function callWasmResourceHandler(
  function updateResourceProviderInstance (line 93) | function updateResourceProviderInstance(
  function cleanupResourceProviders (line 112) | function cleanupResourceProviders(pluginId: string, app?: any): void {
  function createResourceProviderBinding (line 142) | function createResourceProviderBinding(

FILE: src/wasm/bindings/socket-manager.ts
  type BufferedDatagram (line 20) | interface BufferedDatagram {
  type PendingOption (line 30) | interface PendingOption {
  type ManagedSocket (line 46) | interface ManagedSocket {
  class SocketManager (line 60) | class SocketManager {
    method createSocket (line 70) | createSocket(pluginId: string, type: 'udp4' | 'udp6' = 'udp4'): number {
    method bind (line 128) | bind(socketId: number, port: number, address?: string): Promise<number> {
    method joinMulticast (line 224) | joinMulticast(
    method leaveMulticast (line 271) | leaveMulticast(
    method setMulticastTTL (line 314) | setMulticastTTL(socketId: number, ttl: number): number {
    method setMulticastLoopback (line 336) | setMulticastLoopback(socketId: number, enabled: boolean): number {
    method setBroadcast (line 358) | setBroadcast(socketId: number, enabled: boolean): number {
    method send (line 388) | send(
    method receive (line 418) | receive(socketId: number): BufferedDatagram | null {
    method getBufferedCount (line 431) | getBufferedCount(socketId: number): number {
    method close (line 440) | close(socketId: number): void {
    method closeAllForPlugin (line 468) | closeAllForPlugin(pluginId: string): void {
    method getStats (line 484) | getStats(): {
  type ManagedTcpSocket (line 510) | interface ManagedTcpSocket {
  class TcpSocketManager (line 530) | class TcpSocketManager {
    method createSocket (line 539) | createSocket(pluginId: string): number {
    method connect (line 629) | connect(socketId: number, address: string, port: number): number {
    method isConnected (line 664) | isConnected(socketId: number): number {
    method send (line 678) | send(socketId: number, data: Buffer): Promise<number> {
    method receiveLine (line 709) | receiveLine(socketId: number): string | null {
    method receiveRaw (line 724) | receiveRaw(socketId: number): Buffer | null {
    method setLineBuffering (line 740) | setLineBuffering(socketId: number, lineBuffering: boolean): number {
    method getBufferedCount (line 755) | getBufferedCount(socketId: number): number {
    method getError (line 766) | getError(socketId: number): string | null {
    method close (line 775) | close(socketId: number): void {
    method closeAllForPlugin (line 794) | closeAllForPlugin(pluginId: string): void {
    method getStats (line 810) | getStats(): {

FILE: src/wasm/bindings/weather-provider.ts
  function callWasmWeatherHandler (line 24) | async function callWasmWeatherHandler(
  function updateWeatherProviderInstance (line 143) | function updateWeatherProviderInstance(
  function cleanupWeatherProviders (line 159) | function cleanupWeatherProviders(pluginId: string, app?: any): void {
  function createWeatherProviderBinding (line 189) | function createWeatherProviderBinding(

FILE: src/wasm/index.ts
  function initializeWasm (line 18) | function initializeWasm(): {

FILE: src/wasm/loader/plugin-config.ts
  function updateWasmPluginConfig (line 19) | async function updateWasmPluginConfig(
  function setWasmPluginEnabled (line 82) | async function setWasmPluginEnabled(

FILE: src/wasm/loader/plugin-lifecycle.ts
  function startWasmPlugin (line 37) | async function startWasmPlugin(
  function startWasmPluginInternal (line 72) | async function startWasmPluginInternal(
  function stopWasmPlugin (line 189) | async function stopWasmPlugin(pluginId: string): Promise<void> {
  function unloadWasmPlugin (line 247) | async function unloadWasmPlugin(
  function reloadWasmPlugin (line 323) | async function reloadWasmPlugin(
  function handleWasmPluginCrash (line 380) | async function handleWasmPluginCrash(
  function shutdownAllWasmPlugins (line 431) | async function shutdownAllWasmPlugins(): Promise<void> {

FILE: src/wasm/loader/plugin-registry.ts
  function initializeLifecycleFunctions (line 50) | function initializeLifecycleFunctions(
  function setPluginStatus (line 70) | function setPluginStatus(
  function addNodejsPluginCompat (line 82) | function addNodejsPluginCompat(plugin: WasmPlugin, pluginId: string): vo...
  function mountWasmWebapp (line 104) | function mountWasmWebapp(
  function registerWasmPlugin (line 162) | async function registerWasmPlugin(
  function getAllWasmPlugins (line 414) | function getAllWasmPlugins(): WasmPlugin[] {
  function getWasmPlugin (line 421) | function getWasmPlugin(pluginId: string): WasmPlugin | undefined {

FILE: src/wasm/loader/plugin-routes.ts
  function backwardsCompat (line 31) | function backwardsCompat(url: string) {
  function handleLogViewerRequest (line 39) | async function handleLogViewerRequest(
  function setupPluginSpecificRoutes (line 156) | function setupPluginSpecificRoutes(plugin: WasmPlugin): void {
  function setupWasmPluginRoutes (line 484) | function setupWasmPluginRoutes(

FILE: src/wasm/loader/types.ts
  type WasmPluginMetadata (line 18) | interface WasmPluginMetadata {
  type WasmPlugin (line 31) | interface WasmPlugin {

FILE: src/wasm/loaders/standard-loader.ts
  function loadStandardPlugin (line 29) | async function loadStandardPlugin(
  function createPluginExports (line 265) | function createPluginExports(

FILE: src/wasm/types.ts
  type WasmCapabilities (line 11) | interface WasmCapabilities {
  type WasmFormat (line 28) | type WasmFormat = 'wasi-p1' | 'unknown'
  type WasmPluginInstance (line 33) | interface WasmPluginInstance {
  type WasmPluginExports (line 52) | interface WasmPluginExports {
  type WasmResourceProvider (line 73) | interface WasmResourceProvider {
  type WasmWeatherProvider (line 83) | interface WasmWeatherProvider {
  type WasmRadarProvider (line 93) | interface WasmRadarProvider {
  type LoaderContext (line 103) | interface LoaderContext {

FILE: src/wasm/utils/fetch-wrapper.ts
  function getNodeFetch (line 16) | function getNodeFetch(): typeof fetch {

FILE: src/wasm/utils/format-detection.ts
  function detectWasmFormat (line 13) | function detectWasmFormat(buffer: Buffer): WasmFormat {

FILE: src/wasm/wasm-runtime.ts
  class WasmRuntime (line 51) | class WasmRuntime {
    method constructor (line 55) | constructor() {
    method isEnabled (line 62) | isEnabled(): boolean {
    method setEnabled (line 69) | setEnabled(enabled: boolean): void {
    method loadPlugin (line 77) | async loadPlugin(
    method unloadPlugin (line 137) | async unloadPlugin(pluginId: string, app?: any): Promise<void> {
    method reloadPlugin (line 175) | async reloadPlugin(pluginId: string): Promise<WasmPluginInstance> {
    method getInstance (line 193) | getInstance(pluginId: string): WasmPluginInstance | undefined {
    method getAllInstances (line 200) | getAllInstances(): WasmPluginInstance[] {
    method isPluginLoaded (line 207) | isPluginLoaded(pluginId: string): boolean {
    method shutdown (line 214) | async shutdown(): Promise<void> {
  function getWasmRuntime (line 237) | function getWasmRuntime(): WasmRuntime {
  function initializeWasmRuntime (line 247) | function initializeWasmRuntime(): WasmRuntime {
  function resetWasmRuntime (line 261) | function resetWasmRuntime(): void {

FILE: src/wasm/wasm-serverapi.ts
  type ServerAPIBridge (line 23) | interface ServerAPIBridge {
  function createServerAPIBridge (line 34) | function createServerAPIBridge(
  function createWasmImports (line 228) | function createWasmImports(
  function readStringFromMemory (line 334) | function readStringFromMemory(
  function writeStringToMemory (line 348) | function writeStringToMemory(
  function callWasmExport (line 371) | function callWasmExport<T>(

FILE: src/wasm/wasm-storage.ts
  type PluginStoragePaths (line 17) | interface PluginStoragePaths {
  function getPluginStoragePaths (line 40) | function getPluginStoragePaths(
  function initializePluginVfs (line 71) | function initializePluginVfs(paths: PluginStoragePaths): void {
  function readPluginConfig (line 102) | function readPluginConfig(configFile: string): any {
  function writePluginConfig (line 127) | function writePluginConfig(configFile: string, config: any): void {
  function migrateFromNodeJs (line 149) | function migrateFromNodeJs(
  function cleanupVfsTmp (line 192) | function cleanupVfsTmp(vfsTmpDir: string): void {
  function getVfsDiskUsage (line 224) | function getVfsDiskUsage(vfsRoot: string): {
  function deletePluginVfs (line 263) | function deletePluginVfs(paths: PluginStoragePaths): void {

FILE: src/wasm/wasm-subscriptions.ts
  type DeltaSubscription (line 15) | interface DeltaSubscription {
  type Delta (line 21) | interface Delta {
  class WasmSubscriptionManager (line 33) | class WasmSubscriptionManager {
    method register (line 46) | register(
    method unregister (line 68) | unregister(pluginId: string): void {
    method getSubscriptions (line 77) | getSubscriptions(pluginId: string): DeltaSubscription[] {
    method matchesPattern (line 84) | private matchesPattern(path: string, pattern: string): boolean {
    method routeDelta (line 103) | routeDelta(delta: Delta): void {
    method startBuffering (line 140) | startBuffering(pluginId: string): void {
    method stopBuffering (line 149) | stopBuffering(pluginId: string): Delta[] {
    method bufferDelta (line 163) | private bufferDelta(pluginId: string, delta: Delta): void {
    method redirectToBuffer (line 182) | redirectToBuffer(pluginId: string): void {
    method restore (line 189) | restore(pluginId: string): void {
    method replayBuffered (line 196) | replayBuffered(pluginId: string, callback: (delta: Delta) => void): vo...
    method getStats (line 215) | getStats(): {
    method clear (line 242) | clear(): void {
  function getSubscriptionManager (line 256) | function getSubscriptionManager(): WasmSubscriptionManager {
  function initializeSubscriptionManager (line 266) | function initializeSubscriptionManager(): WasmSubscriptionManager {
  function resetSubscriptionManager (line 281) | function resetSubscriptionManager(): void {

FILE: src/zip.ts
  type ZipFile (line 5) | interface ZipFile {
  type ZipOptions (line 10) | interface ZipOptions {
  function sendZip (line 19) | function sendZip(res: Response, options: ZipOptions): void {

FILE: src/zones.ts
  type ZoneMethods (line 7) | interface ZoneMethods {
  class Zones (line 16) | class Zones {
    method constructor (line 20) | constructor(
    method sendNormalDelta (line 40) | sendNormalDelta(path: Path) {
    method watchForZones (line 55) | watchForZones(path: Path, zones: Zone[], methods: ZoneMethods) {
  function getMethod (line 86) | function getMethod(state: string, methods: ZoneMethods): ALARM_METHOD[] {
  function getNotificationDelta (line 100) | function getNotificationDelta(

FILE: test/BackpressureManager.ts
  type MockTransport (line 10) | interface MockTransport extends BackpressureTransport {
  function createMockTransport (line 17) | function createMockTransport(bufferLength = 0): MockTransport {
  function createDelta (line 37) | function createDelta(path: string, value: unknown, source?: string): Del...

FILE: test/applicationData.ts
  constant APP_ID (line 23) | const APP_ID = 'testApplication'
  constant APP_VERSION (line 24) | const APP_VERSION = '1.0.0'
  type TestCase (line 26) | interface TestCase {
  function post (line 98) | async function post(globalOrUser: boolean, token: string, expected: numb...
  function readUserData (line 131) | function readUserData(test: TestCase, userName: string) {
  function fail (line 149) | async function fail(appid: string, version: string) {

FILE: test/chart-tile-regex.ts
  constant MAX_REGEX_MATCH_MS (line 6) | const MAX_REGEX_MATCH_MS = 50

FILE: test/deltaPriority.ts
  function push (line 56) | function push(sourceRef: string, delay: number, shouldBeEmitted: boolean) {

FILE: test/endpoint-auth.ts
  function authHeaders (line 31) | function authHeaders(token: string) {
  function fetchEndpoint (line 38) | async function fetchEndpoint(

FILE: test/filter-test-helper.ts
  function filter (line 3) | function filter(regexp: string, input: string): Promise<string> {

FILE: test/history-api.ts
  constant FROM (line 20) | const FROM = '2025-01-01T00:00:00Z'
  function assertSchema (line 23) | function assertSchema(schema: TSchema, value: unknown, name: string) {
  function mkDirSync (line 33) | function mkDirSync(dirPath: string) {

FILE: test/httpprovider.js
  function HttpProvider (line 20) | function HttpProvider(options) {
  function handleDelta (line 37) | function handleDelta(req, res) {

FILE: test/metadata-e2e.ts
  constant TEST_PATH_DOTS (line 12) | const TEST_PATH_DOTS = 'a.test.path'
  constant TEST_PATH_SLASHES (line 13) | const TEST_PATH_SLASHES = 'a/test/path'

FILE: test/metadata.js
  function getUrl (line 107) | function getUrl(url) {

FILE: test/multiple-values.js
  function removeDisplayUnits (line 37) | function removeDisplayUnits(tree) {

FILE: test/nmea0183-filtering.ts
  function nmea0183filter (line 5) | function nmea0183filter(sentence: string, input: string) {

FILE: test/oidc/crypto-service.test.ts
  function deriveSecret (line 24) | function deriveSecret(masterKey: string, domain: string): string {

FILE: test/oidc/discovery.test.ts
  function createMockFetch (line 24) | function createMockFetch(

FILE: test/oidc/id-token-validation.test.ts
  type JoseModule (line 25) | type JoseModule = typeof import('jose')
  function createIdToken (line 84) | async function createIdToken(
  function setupJwksMock (line 102) | function setupJwksMock() {

FILE: test/oidc/oidc-auth.test.ts
  type RegisteredRoute (line 20) | interface RegisteredRoute {
  function createMockRequest (line 84) | function createMockRequest(overrides: Partial<Request> = {}): Request {
  type MockResponse (line 99) | interface MockResponse {
  function createMockResponse (line 109) | function createMockResponse(): MockResponse {
  function findRoute (line 131) | function findRoute(

FILE: test/oidc/user-info.test.ts
  function createTestJwt (line 7) | function createTestJwt(payload: object): string {

FILE: test/oidc/user-service.test.ts
  function createInMemoryUserService (line 10) | function createInMemoryUserService(): ExternalUserService & {

FILE: test/plugin-crash-isolation.ts
  type PluginInfo (line 9) | interface PluginInfo {
  type ProviderStatus (line 14) | interface ProviderStatus {

FILE: test/plugins.js
  function mkDirSync (line 118) | function mkDirSync(dirPath) {
  function writePluginConfig (line 128) | function writePluginConfig(config) {
  function postPluginConfig (line 137) | async function postPluginConfig(port, config) {

FILE: test/providers.js
  function checkExistingProvider (line 171) | function checkExistingProvider(existing) {

FILE: test/put.js
  function switch2Handler (line 31) | function switch2Handler(context, path, value, cb) {
  function WsPromiser (line 337) | function WsPromiser(url) {

FILE: test/rate-limit.ts
  type ServerInstance (line 8) | interface ServerInstance {
  function wsLogin (line 12) | function wsLogin(
  function openWs (line 44) | function openWs(port: number): Promise<WebSocket> {
  constant LOGIN_MAX (line 68) | const LOGIN_MAX = 100
  constant API_MAX (line 69) | const API_MAX = 1000

FILE: test/seatalk1-filtering.ts
  function seatalk1filter (line 5) | function seatalk1filter(command: string, input: string) {

FILE: test/security.js
  function login (line 141) | async function login(username, password) {
  function formLoginWithDestination (line 350) | async function formLoginWithDestination(username, password, destination) {

FILE: test/servertestutilities.js
  function WsPromiser (line 36) | function WsPromiser(url, timeout = 250) {
  constant WRITE_USER_NAME (line 101) | const WRITE_USER_NAME = 'writeuser'
  constant WRITE_USER_PASSWORD (line 102) | const WRITE_USER_PASSWORD = 'writepass'
  constant LIMITED_USER_NAME (line 103) | const LIMITED_USER_NAME = 'testuser'
  constant LIMITED_USER_PASSWORD (line 104) | const LIMITED_USER_PASSWORD = 'verylimited'
  constant ADMIN_USER_NAME (line 105) | const ADMIN_USER_NAME = 'adminuser'
  constant ADMIN_USER_PASSWORD (line 106) | const ADMIN_USER_PASSWORD = 'admin'
  constant NOPASSWORD_USER_NAME (line 107) | const NOPASSWORD_USER_NAME = 'nopassword'
  function login (line 217) | function login(server, username, password) {

FILE: test/sliding-session.ts
  type TestServer (line 10) | interface TestServer {
  function getSecretKey (line 20) | function getSecretKey(server: TestServer): string {
  function signToken (line 24) | function signToken(
  function findJauthCookie (line 45) | function findJauthCookie(res: Response): string | undefined {
  function extractTokenFromCookie (line 51) | function extractTokenFromCookie(cookie: string): string {
  function login (line 81) | async function login(rememberMe: boolean): Promise<Response> {
  function authenticatedGet (line 93) | async function authenticatedGet(token: string): Promise<Response> {

FILE: test/subscriptions.js
  function getDelta (line 7) | function getDelta(overwrite) {
  function getEmptyPathDelta (line 70) | function getEmptyPathDelta(overwrite) {
  function getClosePosistionDelta (line 99) | function getClosePosistionDelta(overwrite) {
  function getFarPosistionDelta (line 127) | function getFarPosistionDelta() {
  function getNullPositionDelta (line 155) | function getNullPositionDelta(overwrite) {
  function testSelfData (line 204) | async function testSelfData(url) {

FILE: test/ts-servertestutilities.ts
  constant DATETIME_REGEX (line 14) | const DATETIME_REGEX = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)Z?$/
  function freeport (line 123) | function freeport(): Promise<number> {

FILE: test/unitpreferences.ts
  constant UNITPREFS_DIR (line 17) | const UNITPREFS_DIR = path.join(__dirname, '../unitpreferences')

FILE: test/wasm-plugins.ts
  type PluginInfo (line 21) | interface PluginInfo {
  type ServerInstance (line 33) | interface ServerInstance {

FILE: test/ws-connection-limit.ts
  constant WS_STREAM_PATH (line 9) | const WS_STREAM_PATH =
  function openWs (line 12) | function openWs(url: string): Promise<WebSocket> {
  function openWsExpect429 (line 27) | function openWsExpect429(
  function openWsWithForwardedIp (line 125) | function openWsWithForwardedIp(
  function openWsWithForwardedIpExpect429 (line 145) | function openWsWithForwardedIpExpect429(

FILE: test/zones.ts
  type MockStreambundle (line 7) | interface MockStreambundle {
  function asNotification (line 13) | function asNotification(value: unknown): Notification {
Condensed preview — 687 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (9,977K chars).
[
  {
    "path": ".coderabbit.yaml",
    "chars": 3805,
    "preview": "language: en-US\n\nreviews:\n  profile: assertive\n  auto_review:\n    enabled: true\n    drafts: false\n  high_level_summary_i"
  },
  {
    "path": ".dockerignore",
    "chars": 27,
    "preview": "node_modules\npackages\nwork\n"
  },
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".gitattributes",
    "chars": 483,
    "preview": "# Default: normalize line endings to LF in the repo, auto-detect on checkout\n* text=auto eol=lf\n\n# Force LF for files th"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 27,
    "preview": "github: [sbender9, tkurki]\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 674,
    "preview": "version: 2\nupdates:\n  # Enable version updates for npm\n  - package-ecosystem: 'npm'\n    # Look for `package.json` and `l"
  },
  {
    "path": ".github/workflows/build-base-image.yml",
    "chars": 4375,
    "preview": "name: Build Docker base images\n\non:\n  schedule:\n    - cron: '0 0 * * 1'\n  workflow_dispatch:\n\njobs:\n  build-images:\n    "
  },
  {
    "path": ".github/workflows/build-docker.yml",
    "chars": 8143,
    "preview": "name: Build Docker development container\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-pro"
  },
  {
    "path": ".github/workflows/plugin-ci.yml",
    "chars": 78867,
    "preview": "# SignalK Plugin CI - Reusable Workflow\n#\n# This workflow lives in the SignalK/signalk-server repository.\n# Plugin devel"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 11212,
    "preview": "name: Release - build & publish modules and server, build & publish docker containers\n\non:\n  push:\n    tags:\n      - 'v*"
  },
  {
    "path": ".github/workflows/require_pr_label.yml",
    "chars": 352,
    "preview": "name: Pull Request Labels\non:\n  pull_request:\n    types: [opened, labeled, unlabeled, synchronize]\njobs:\n  label:\n    ru"
  },
  {
    "path": ".github/workflows/security-scan.yml",
    "chars": 1287,
    "preview": "name: Security Scan for Docker Images\n\non:\n  schedule:\n    - cron: '0 0 * * 1'\n  workflow_dispatch:\n\npermissions:\n  cont"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 800,
    "preview": "name: CI test\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_req"
  },
  {
    "path": ".gitignore",
    "chars": 946,
    "preview": "node_modules\n!test/plugin-test-config/node_modules/\n\n*.tsbuildinfo\n\nlib/\ndist/\n\n.DS_Store\n.vscode/\n*.db\nlogs/*\nbower_com"
  },
  {
    "path": ".mocharc.js",
    "chars": 120,
    "preview": "module.exports = {\n  require: ['ts-node/register'],\n  extensions: ['ts', 'tsx', 'js'],\n  timeout: 20000,\n  exit: true\n}\n"
  },
  {
    "path": ".npmignore",
    "chars": 1135,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
  },
  {
    "path": ".npmrc",
    "chars": 41,
    "preview": "package-lock=false\nlegacy-peer-deps=true\n"
  },
  {
    "path": ".nvmrc",
    "chars": 3,
    "preview": "24\n"
  },
  {
    "path": ".prettierignore",
    "chars": 132,
    "preview": "*.guard.ts\npublic\n\n**/*.mbtiles\n**/*.pmtiles\n**/.__mf__temp\n# AssemblyScript build outputs\npackages/assemblyscript-plugi"
  },
  {
    "path": ".prettierrc.json",
    "chars": 218,
    "preview": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"none\",\n  \"overrides\": [\n    {\n      \"files\": [\"**/assembly"
  },
  {
    "path": ".python-version",
    "chars": 5,
    "preview": "3.11\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 6720,
    "preview": "# Signal K Server\n\nSignal K Server is the reference implementation of a [Signal K](https://signalk.org/) server. Signal "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 105,
    "preview": "## Please see [Releases](https://github.com/SignalK/signalk-server-node/releases) for the release notes.\n"
  },
  {
    "path": "CLAUDE.md",
    "chars": 11,
    "preview": "@AGENTS.md\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 6867,
    "preview": "---\ntitle: Contributing\n---\n\n# Contributing\n\nSignal K server is an Open Source project and contributions are welcome.\n\nC"
  },
  {
    "path": "LICENSE",
    "chars": 10172,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Procfile",
    "chars": 71,
    "preview": "web: node bin/signalk-server -s ./settings/n2k-from-file-settings.json\n"
  },
  {
    "path": "README.md",
    "chars": 16378,
    "preview": "# Signal K Server\n\n![Signal K logo](https://user-images.githubusercontent.com/5200296/226164888-d33b2349-e608-4bed-965f-"
  },
  {
    "path": "docker/Dockerfile",
    "chars": 1650,
    "preview": "ARG REGISTRY=\"cr.signalk.io\"\nARG BASE_IMAGE=\"24.04-22.x\"\n\nFROM ${REGISTRY}/signalk/signalk-server-base:latest-${BASE_IMA"
  },
  {
    "path": "docker/Dockerfile_base_24.04",
    "chars": 1262,
    "preview": "FROM ubuntu:24.04\nARG NODE\nENV DEBIAN_FRONTEND=noninteractive\n\nRUN userdel -r ubuntu \\\n  && groupadd --gid 1000 node \\\n "
  },
  {
    "path": "docker/Dockerfile_base_alpine",
    "chars": 1170,
    "preview": "ARG NODE\nFROM node:${NODE}-alpine\n\nCOPY docker/avahi/avahi-dbus.conf docker/bluez/bluezuser.conf /etc/dbus-1/system.d/\n\n"
  },
  {
    "path": "docker/Dockerfile_rel",
    "chars": 1201,
    "preview": "ARG REGISTRY=\"cr.signalk.io\"\nARG BASE_IMAGE=\"24.04-22.x\"\n\nFROM ${REGISTRY}/signalk/signalk-server-base:latest-${BASE_IMA"
  },
  {
    "path": "docker/README.md",
    "chars": 4813,
    "preview": "# General\n\nRelease process first publishes the server's modules to npm. Docker images are then built using the just publ"
  },
  {
    "path": "docker/avahi/avahi-dbus.conf",
    "chars": 1220,
    "preview": "<!DOCTYPE busconfig PUBLIC\n          \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\"\n          \"http://www.freedes"
  },
  {
    "path": "docker/bluez/bluezuser.conf",
    "chars": 721,
    "preview": "<!-- This configuration file specifies the required security policies\nfor a user to communicate with BlueZ. -->\n \n<!DOCT"
  },
  {
    "path": "docker/docker-compose.yml",
    "chars": 1230,
    "preview": "version: '2.2'\nservices:\n  signalk-server:\n    image: cr.signalk.io/signalk/signalk-server:latest\n    container_name: si"
  },
  {
    "path": "docker/startup.sh",
    "chars": 2304,
    "preview": "#!/usr/bin/env sh\n\n# Detect container runtime (only if not already set by user)\nif [ -z \"$CONTAINER_RUNTIME\" ]; then\n   "
  },
  {
    "path": "docker/v2_demo/Dockerfile",
    "chars": 700,
    "preview": "# docker buildx build --platform linux/amd64 -f  Dockerfile_heroku_api_demo -t registry.heroku.com/signalk-course-resour"
  },
  {
    "path": "docker/v2_demo/course-data.json",
    "chars": 154,
    "preview": "{\n  \"configuration\": {\n    \"notifications\": {},\n    \"calculations\": {\n      \"method\": \"Rhumbline\",\n      \"autopilot\": tr"
  },
  {
    "path": "docker/v2_demo/resources/routes/ad825f6c-1ae9-4f76-abc4-df2866b14b78",
    "chars": 450,
    "preview": "{\"distance\":18912,\"name\":\"test route\",\"description\":\"testing route stuff\",\"feature\":{\"type\":\"Feature\",\"geometry\":{\"type\""
  },
  {
    "path": "docker/v2_demo/resources/routes/da825f6c-1ae9-4f76-abc4-df2866b14b78",
    "chars": 450,
    "preview": "{\"distance\":18912,\"name\":\"test route\",\"description\":\"testing route stuff\",\"feature\":{\"type\":\"Feature\",\"geometry\":{\"type\""
  },
  {
    "path": "docker/v2_demo/resources/waypoints/ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a",
    "chars": 295,
    "preview": "{\n    \"name\": \"demo waypoint\",\n    \"description\": \"\",\n    \"feature\": {\n      \"type\": \"Feature\",\n      \"geometry\": {\n    "
  },
  {
    "path": "docker/v2_demo/resources/waypoints/afe46290-aa98-4d2f-9c04-d199ca64942e",
    "chars": 380,
    "preview": "{\n    \"name\": \"lock\",\n    \"description\": \"this is the lock\",\n    \"feature\": {\n      \"type\": \"Feature\",\n      \"geometry\":"
  },
  {
    "path": "docker/v2_demo/resources-provider.json",
    "chars": 188,
    "preview": "{\n  \"configuration\": {\n    \"standard\": {\n      \"routes\": true,\n      \"waypoints\": true,\n      \"notes\": true,\n      \"regi"
  },
  {
    "path": "docker/v2_demo/serverstate/course/settings.json",
    "chars": 565,
    "preview": "{\n  \"activeRoute\": {\n    \"href\": \"/resources/routes/ad825f6c-1ae9-4f76-abc4-df2866b14b78\",\n    \"startTime\": \"2022-04-21T"
  },
  {
    "path": "docker/v2_demo/startup_heroku_demo.sh",
    "chars": 166,
    "preview": "#!/usr/bin/env sh\nservice dbus restart\n/usr/sbin/avahi-daemon -k\n/usr/sbin/avahi-daemon --no-drop-root &\n/home/node/sign"
  },
  {
    "path": "docs/README.md",
    "chars": 1414,
    "preview": "---\ntitle: Introduction\n---\n\n# Introduction\n\nSignal K Server is software designed to be deployed on a vessel to act as a"
  },
  {
    "path": "docs/breaking_changes.md",
    "chars": 5995,
    "preview": "---\ntitle: Breaking Changes\n---\n\n# Breaking Changes & Deprecations\n\nThis document lists breaking changes and deprecation"
  },
  {
    "path": "docs/develop/README.md",
    "chars": 4040,
    "preview": "---\ntitle: Developing\nchildren:\n  - ../whats_new.md\n  - ../breaking_changes.md\n  - plugins/README.md\n  - rest-api/README"
  },
  {
    "path": "docs/develop/plugins/README.md",
    "chars": 12271,
    "preview": "---\ntitle: Plugins\nchildren:\n  - ../webapps.md\n  - wasm/README.md\n  - deltas.md\n  - configuration.md\n  - backpressure.md"
  },
  {
    "path": "docs/develop/plugins/autopilot_provider_plugins.md",
    "chars": 5108,
    "preview": "---\ntitle: Autopilot Providers\n---\n\n# Autopilot Provider plugins\n\nThe Signal K [Autopilot API](../rest-api/autopilot_api"
  },
  {
    "path": "docs/develop/plugins/backpressure.md",
    "chars": 4822,
    "preview": "---\ntitle: Connection Backpressure\n---\n\n# Connection Backpressure\n\nSignal K Server includes automatic backpressure handl"
  },
  {
    "path": "docs/develop/plugins/ci.md",
    "chars": 11063,
    "preview": "---\ntitle: Plugin CI/CD\n---\n\n# Continuous Integration for Plugins\n\nSignal K provides a reusable GitHub Actions workflow "
  },
  {
    "path": "docs/develop/plugins/configuration.md",
    "chars": 2542,
    "preview": "---\ntitle: Configuration\n---\n\n# Plugin Configuration\n\nA plugin's {@link @signalk/server-api!Plugin.schema | `schema`} fu"
  },
  {
    "path": "docs/develop/plugins/course_calculations.md",
    "chars": 2685,
    "preview": "---\ntitle: Course Providers\n---\n\n# Course Calculations and Providers\n\nThe _Course API_ defines the path `/vessels/self/n"
  },
  {
    "path": "docs/develop/plugins/custom_renderers.md",
    "chars": 3365,
    "preview": "---\ntitle: Custom Renderers for the Data Browser\n---\n\n# Custom Renderers\n\nSignalk's Data Browser provides an easily navi"
  },
  {
    "path": "docs/develop/plugins/deltas.md",
    "chars": 6809,
    "preview": "---\ntitle: Processing Data\n---\n\n# Processing data from the server\n\nA plugin will generally want to:\n\n1. Subscribe to dat"
  },
  {
    "path": "docs/develop/plugins/examples/plugin-caller-example.yml",
    "chars": 3312,
    "preview": "# ─────────────────────────────────────────────────────────────\n# SignalK Plugin CI\n#\n# Drop this file into your plugin "
  },
  {
    "path": "docs/develop/plugins/examples/plugin-dependabot-example.yml",
    "chars": 1112,
    "preview": "# ─────────────────────────────────────────────────────────────\n# SignalK Plugin Dependabot Config\n#\n# Drop this file in"
  },
  {
    "path": "docs/develop/plugins/examples/plugin-release-example.yml",
    "chars": 2521,
    "preview": "# ─────────────────────────────────────────────────────────────\n# SignalK Plugin Release + Publish\n#\n# Drop this file in"
  },
  {
    "path": "docs/develop/plugins/publishing.md",
    "chars": 2820,
    "preview": "---\ntitle: Publishing to The AppStore\n---\n\n# Publishing to The AppStore\n\nPlugins and WebApps are available in the AppSto"
  },
  {
    "path": "docs/develop/plugins/release.md",
    "chars": 5629,
    "preview": "---\ntitle: Releases and Changelogs\n---\n\n# Releases and Changelogs for Plugins\n\nWhen a user updates a plugin through the "
  },
  {
    "path": "docs/develop/plugins/resource_provider_plugins.md",
    "chars": 10579,
    "preview": "---\ntitle: Resource Providers\n---\n\n# Resource Provider plugins\n\nThe Signal K server _Resource API_ provides a common set"
  },
  {
    "path": "docs/develop/plugins/wasm/README.md",
    "chars": 4101,
    "preview": "---\ntitle: WASM Plugins\nchildren:\n  - assemblyscript.md\n  - rust.md\n  - go.md\n  - http_endpoints.md\n  - deltas.md\n  - ca"
  },
  {
    "path": "docs/develop/plugins/wasm/assemblyscript.md",
    "chars": 14524,
    "preview": "---\ntitle: AssemblyScript Plugins\n---\n\n# Creating AssemblyScript Plugins\n\nAssemblyScript is the recommended language for"
  },
  {
    "path": "docs/develop/plugins/wasm/best_practices.md",
    "chars": 5902,
    "preview": "---\ntitle: Best Practices for WASM Plugins\n---\n\n# Best Practices for WASM Plugins\n\n## Hot Reload\n\nWASM plugins support h"
  },
  {
    "path": "docs/develop/plugins/wasm/capabilities.md",
    "chars": 14410,
    "preview": "---\ntitle: Plugin Capabilities\n---\n\n# Plugin Capabilities\n\n## Capability Types\n\nDeclare required capabilities in `packag"
  },
  {
    "path": "docs/develop/plugins/wasm/deltas.md",
    "chars": 6104,
    "preview": "---\ntitle: Deltas\n---\n\n# Working with Signal K Deltas\n\nWASM plugins can both **emit** and **receive** Signal K deltas. T"
  },
  {
    "path": "docs/develop/plugins/wasm/go.md",
    "chars": 6856,
    "preview": "---\ntitle: Go/TinyGo Plugins\n---\n\n# Creating Go/TinyGo Plugins\n\nGo plugins use TinyGo, a Go compiler designed for small "
  },
  {
    "path": "docs/develop/plugins/wasm/http_endpoints.md",
    "chars": 4619,
    "preview": "---\ntitle: HTTP Endpoints\n---\n\n# HTTP Endpoints\n\nWASM plugins can register custom HTTP endpoints to provide REST APIs or"
  },
  {
    "path": "docs/develop/plugins/wasm/integration_guide.md",
    "chars": 10237,
    "preview": "---\ntitle: Integration Guide for WASM Plugins\n---\n\n# Integration Guide for WASM Plugins\n\n## Static File Serving\n\nPlugins"
  },
  {
    "path": "docs/develop/plugins/wasm/rust.md",
    "chars": 11371,
    "preview": "---\ntitle: Rust Plugins\n---\n\n# Creating Rust Plugins\n\nRust is excellent for WASM plugins due to its zero-cost abstractio"
  },
  {
    "path": "docs/develop/plugins/weather_provider_plugins.md",
    "chars": 3027,
    "preview": "---\ntitle: Weather Providers\n---\n\n# Weather Providers\n\nThe Signal K server [Weather API](../rest-api/weather_api.md) pro"
  },
  {
    "path": "docs/develop/rest-api/README.md",
    "chars": 2521,
    "preview": "---\ntitle: REST APIs\nchildren:\n  - conventions.md\n  - autopilot_api.md\n  - course_api.md\n  - history_api.md\n  - notifica"
  },
  {
    "path": "docs/develop/rest-api/autopilot_api.md",
    "chars": 13202,
    "preview": "---\ntitle: Autopilot API\n---\n\n# Autopilot API\n\nThe Autopilot API defines the `autopilots` path under `self` _(e.g. `/sig"
  },
  {
    "path": "docs/develop/rest-api/conventions.md",
    "chars": 2393,
    "preview": "---\ntitle: API Conventions\n---\n\n# Signal K REST API Conventions\n\n## Overview\n\nThis document outlines the conventions use"
  },
  {
    "path": "docs/develop/rest-api/course_api.md",
    "chars": 11155,
    "preview": "---\ntitle: Course API\n---\n\n# Course API\n\nThe _Course API_ provides common course operations under the path `/signalk/v2/"
  },
  {
    "path": "docs/develop/rest-api/history_api.md",
    "chars": 7943,
    "preview": "---\ntitle: History API\n---\n\n# History API\n\nThe _History API_ provides access to historical data, typically stored in a d"
  },
  {
    "path": "docs/develop/rest-api/notifications_api.md",
    "chars": 10825,
    "preview": "---\ntitle: Notifications API\n---\n\n# Notifications API\n\nThe Notifications API enables the raising, actioning and centrali"
  },
  {
    "path": "docs/develop/rest-api/plugin_api.md",
    "chars": 464,
    "preview": "---\ntitle: Plugin API\n---\n\n# Plugin configuration HTTP API\n\n## `GET /plugins/`\n\nGet a list of installed plugins and thei"
  },
  {
    "path": "docs/develop/rest-api/proposed/README.md",
    "chars": 568,
    "preview": "---\ntitle: Proposed APIs\nchildren:\n  - anchor_api.md\n---\n\n# PROPOSED APIs\n\nThe following APIs have been identified for f"
  },
  {
    "path": "docs/develop/rest-api/proposed/anchor_api.md",
    "chars": 889,
    "preview": "---\ntitle: Anchor API\n---\n\n# Anchor API\n\n#### (Proposed)\n\n_Note: The definition of this API is currently under developme"
  },
  {
    "path": "docs/develop/rest-api/radar_api.md",
    "chars": 36319,
    "preview": "---\ntitle: Radar API\n---\n\n# Radar API\n\nThe Signal K server Radar API provides a unified interface for viewing and contro"
  },
  {
    "path": "docs/develop/rest-api/resources_api.md",
    "chars": 8719,
    "preview": "---\ntitle: Resources API\n---\n\n# Working with the Resources API\n\nThe SignalK specification defines a number of resources "
  },
  {
    "path": "docs/develop/rest-api/weather_api.md",
    "chars": 3588,
    "preview": "---\ntitle: Weather API\n---\n\n# Weather API\n\nThe Signal K server Weather API provides a common set of operations for viewi"
  },
  {
    "path": "docs/develop/webapps.md",
    "chars": 14614,
    "preview": "---\ntitle: WebApps\n---\n\n# WebApps and Components\n\nSignal K Server provides the following ways to add web-based user inte"
  },
  {
    "path": "docs/guides/README.md",
    "chars": 1089,
    "preview": "---\ntitle: Guides\nchildren:\n  - anchoralarm/anchoralarm.md\n  - datalogging/datalogging.md\n  - navdataserver/navdataserve"
  },
  {
    "path": "docs/guides/anchoralarm/anchoralarm.md",
    "chars": 13960,
    "preview": "---\ntitle: Anchor Alarm\n---\n\n# Feature: Anchor Alarm\n\nThis document describes how to setup an anchor alarm using Signal "
  },
  {
    "path": "docs/guides/datalogging/datalogging.md",
    "chars": 805,
    "preview": "---\ntitle: Data Logging\n---\n\n# Data Logging\n\nSignal K server can log all input data from the configured input connection"
  },
  {
    "path": "docs/guides/navdataserver/navdataserver.md",
    "chars": 4144,
    "preview": "---\ntitle: 'NMEA0183 Data Server'\ncategory: 'Guides'\n---\n\n# Signal K Server as a NMEA0183 Data Server\n\nThis document det"
  },
  {
    "path": "docs/guides/udev.md",
    "chars": 2663,
    "preview": "---\ntitle: Linux udev Rules\n---\n\n# Linux udev Rules\n\nWhen you connect a USB device to a Linux computer, the kernel will "
  },
  {
    "path": "docs/guides/unitpreferences.md",
    "chars": 10605,
    "preview": "---\ntitle: Unit Preferences\n---\n\n# Unit Preferences\n\nThis guide describes the unit preferences mechanism in Signal K Ser"
  },
  {
    "path": "docs/installation/README.md",
    "chars": 1269,
    "preview": "---\ntitle: Installation\nchildren:\n  - raspberry_pi_installation.md\n  - npm.md\n  - docker.md\n  - source.md\n  - updating.m"
  },
  {
    "path": "docs/installation/command_line.md",
    "chars": 21706,
    "preview": "---\ntitle: Runtime Environment & Options\n---\n\n# Runtime Environment & Options\n\nSignal K Server provides the following co"
  },
  {
    "path": "docs/installation/docker.md",
    "chars": 1106,
    "preview": "---\ntitle: Docker\n---\n\n# Installing from Docker\n\nSignal K Server is available as a Docker image on _Docker Hub_ and _cr."
  },
  {
    "path": "docs/installation/npm.md",
    "chars": 725,
    "preview": "---\ntitle: NPM\n---\n\n# Installing from NPM\n\nSignal K Server can be installed directly using NPM.\n\n## Linux / macOS\n\n```sh"
  },
  {
    "path": "docs/installation/raspberry_pi_installation.md",
    "chars": 4314,
    "preview": "---\ntitle: Raspberry Pi\n---\n\n# Installing on Raspberry Pi\n\nInstallation of Signal K server consists of the following ste"
  },
  {
    "path": "docs/installation/source.md",
    "chars": 932,
    "preview": "---\ntitle: From Source\n---\n\n# Installing from source\n\nInstallation from the GitHub repository is useful when developing "
  },
  {
    "path": "docs/installation/updating.md",
    "chars": 3137,
    "preview": "---\ntitle: Updating your Installation\n---\n\n# Updating your Installation\n\nSignal K Server is frequently updated to introd"
  },
  {
    "path": "docs/internal/README.md",
    "chars": 1110,
    "preview": "# Internal Documentation\n\nThis folder contains internal/maintainer documentation for Signal K Server. These documents de"
  },
  {
    "path": "docs/internal/wasm-architecture.md",
    "chars": 3635,
    "preview": "# WASM Plugin Architecture\n\nInternal documentation for Signal K Server WASM plugin infrastructure.\n\n## Overview\n\nThe WAS"
  },
  {
    "path": "docs/internal/wasm-asyncify.md",
    "chars": 11517,
    "preview": "# Asyncify Implementation for SignalK WASM Plugins\n\n## Overview\n\nThis document describes the implementation of Asyncify "
  },
  {
    "path": "docs/oidc.md",
    "chars": 14426,
    "preview": "---\ntitle: OIDC Authentication\n---\n\n# OpenID Connect (OIDC) Authentication\n\nSignal K Server supports OpenID Connect (OID"
  },
  {
    "path": "docs/security-architecture.md",
    "chars": 6771,
    "preview": "---\ntitle: Security Architecture\n---\n\n# Security Architecture\n\nThis document describes the architecture of Signal K Serv"
  },
  {
    "path": "docs/security.md",
    "chars": 12004,
    "preview": "---\ntitle: Security\nchildren:\n  - setup/generating_tokens.md\n  - oidc.md\n---\n\n# Security\n\nThe umbrella term _Security_ i"
  },
  {
    "path": "docs/setup/configuration.md",
    "chars": 6979,
    "preview": "---\ntitle: Configuration\nchildren:\n  - seatalk/README.md\n  - nmea.md\n---\n\n# Configuring Signal K Server\n\nSignal K Server"
  },
  {
    "path": "docs/setup/generating_tokens.md",
    "chars": 1802,
    "preview": "---\ntitle: Generating Tokens\n---\n\n# Generating Tokens\n\nFor a device to be able to interact with a Signal K server with s"
  },
  {
    "path": "docs/setup/nmea.md",
    "chars": 2592,
    "preview": "---\ntitle: NMEA Connections\n---\n\n# NMEA Connections\n\nMost equipment on boats use NMEA 0183, NMEA 2000, or other propriet"
  },
  {
    "path": "docs/setup/seatalk/README.md",
    "chars": 5981,
    "preview": "---\ntitle: Seatalk Connections\n---\n\n# Seatalk Connections\n\nThe Signal K Server supports a variety of data connection typ"
  },
  {
    "path": "docs/src/features/weather/weather.md",
    "chars": 3074,
    "preview": "# Working with Weather Data\n\n## Introduction\n\nThis document outlines the way in which weather data is managed in Signal "
  },
  {
    "path": "docs/support/help.md",
    "chars": 2349,
    "preview": "---\ntitle: Help & Support\n---\n\n# Help & Support\n\nSignal K has an friendly and helpful community where you can find suppo"
  },
  {
    "path": "docs/support/sponsor.md",
    "chars": 423,
    "preview": "---\ntitle: Sponsor\n---\n\n# Sponsor Signal K\n\nSignal K is all about creating open and interoperable marine data systems. W"
  },
  {
    "path": "docs/whats_new.md",
    "chars": 3136,
    "preview": "---\ntitle: What's New\n---\n\n# What's new in Version 2.\n\nSignal K Server version 2 introduces new REST APIs designed to pe"
  },
  {
    "path": "empty_file",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "eslint.config.js",
    "chars": 4010,
    "preview": "const { defineConfig, globalIgnores } = require('eslint/config')\nconst js = require('@eslint/js')\nconst globals = requir"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/.gitignore",
    "chars": 207,
    "preview": "# Rust build artifacts\ntarget/\nCargo.lock\n\n# Editor/IDE files\n.idea/\n.vscode/\n*.swp\n*.swo\n*~\n\n# OS files\n.DS_Store\nThumb"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/.npmignore",
    "chars": 334,
    "preview": "# Rust build artifacts\ntarget/\nCargo.lock\n\n# Editor/IDE files\n.idea/\n.vscode/\n*.swp\n*.swo\n*~\n\n# OS files\n.DS_Store\nThumb"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/Cargo.toml",
    "chars": 422,
    "preview": "[package]\nname = \"anchor-watch-rust\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"Anchor Watch WASM plugin for Sign"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/README.md",
    "chars": 11272,
    "preview": "# Example Anchor Watch - Rust WASM Plugin\n\nA Signal K WASM plugin written in Rust demonstrating:\n\n- Rust WASM compilatio"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/package.json",
    "chars": 1083,
    "preview": "{\n  \"name\": \"@signalk/example-anchor-watch-rust\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Anchor Watch WASM plugin for S"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/src/lib.rs",
    "chars": 19171,
    "preview": "//! Anchor Watch WASM Plugin for Signal K\n//!\n//! A Rust implementation demonstrating:\n//! - WASM plugin architecture wi"
  },
  {
    "path": "examples/wasm-plugins/example-anchor-watch-rust/wit/signalk-plugin.wit",
    "chars": 1606,
    "preview": "// Signal K WASM Plugin Interface Definition for Rust\n// This WIT file defines the interface that Signal K WASM plugins "
  },
  {
    "path": "examples/wasm-plugins/example-hello-assemblyscript/.gitignore",
    "chars": 124,
    "preview": "# Build artifacts\nbuild/\nplugin.js\nplugin.d.ts\n*.wasm\n\n# Dependencies\nnode_modules/\npackage-lock.json\n\n# OS files\n.DS_St"
  },
  {
    "path": "examples/wasm-plugins/example-hello-assemblyscript/.npmignore",
    "chars": 20,
    "preview": "# test builds\n*.tgz\n"
  },
  {
    "path": "examples/wasm-plugins/example-hello-assemblyscript/README.md",
    "chars": 5311,
    "preview": "# Example Hello AssemblyScript - Signal K WASM Plugin\n\nA minimal example of a Signal K WASM plugin written in AssemblySc"
  },
  {
    "path": "examples/wasm-plugins/example-hello-assemblyscript/asconfig.json",
    "chars": 474,
    "preview": "{\n  \"targets\": {\n    \"release\": {\n      \"outFile\": \"plugin.wasm\",\n      \"sourceMap\": false,\n      \"optimize\": true,\n    "
  },
  {
    "path": "examples/wasm-plugins/example-hello-assemblyscript/assembly/index.ts",
    "chars": 9223,
    "preview": "/**\n * Hello World - AssemblyScript WASM Plugin\n *\n * Demonstrates basic AssemblyScript plugin structure for Signal K\n *"
  },
  {
    "path": "examples/wasm-plugins/example-hello-assemblyscript/package.json",
    "chars": 905,
    "preview": "{\n  \"name\": \"@signalk/example-hello-assemblyscript\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Hello World WASM plugin wri"
  },
  {
    "path": "examples/wasm-plugins/example-routes-waypoints/.gitignore",
    "chars": 102,
    "preview": "# Build artifacts\nbuild/\n*.wasm\n\n# Dependencies\nnode_modules/\npackage-lock.json\n\n# OS files\n.DS_Store\n"
  },
  {
    "path": "examples/wasm-plugins/example-routes-waypoints/.npmignore",
    "chars": 184,
    "preview": "# Source files - not needed for runtime\nassembly/\nasconfig.json\n\n# Dev/build artifacts\nnode_modules/\n*.tgz\n*.debug.wasm\n"
  },
  {
    "path": "examples/wasm-plugins/example-routes-waypoints/README.md",
    "chars": 5516,
    "preview": "# Routes & Waypoints Resource Provider Plugin Example\n\nThis example demonstrates how to create a WASM plugin that provid"
  },
  {
    "path": "examples/wasm-plugins/example-routes-waypoints/asconfig.json",
    "chars": 458,
    "preview": "{\n  \"targets\": {\n    \"release\": {\n      \"outFile\": \"build/plugin.wasm\",\n      \"sourceMap\": false,\n      \"optimize\": true"
  },
  {
    "path": "examples/wasm-plugins/example-routes-waypoints/assembly/index.ts",
    "chars": 15615,
    "preview": "/**\n * Routes & Waypoints Resource Provider Plugin Example\n *\n * Demonstrates:\n * - Resource Provider capability for sta"
  },
  {
    "path": "examples/wasm-plugins/example-routes-waypoints/package.json",
    "chars": 1047,
    "preview": "{\n  \"name\": \"@signalk/example-routes-waypoints\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Example WASM plugin demonstrati"
  },
  {
    "path": "examples/wasm-plugins/example-weather-plugin/.gitignore",
    "chars": 102,
    "preview": "# Build artifacts\nbuild/\n*.wasm\n\n# Dependencies\nnode_modules/\npackage-lock.json\n\n# OS files\n.DS_Store\n"
  },
  {
    "path": "examples/wasm-plugins/example-weather-plugin/.npmignore",
    "chars": 20,
    "preview": "# test builds\n*.tgz\n"
  },
  {
    "path": "examples/wasm-plugins/example-weather-plugin/README.md",
    "chars": 2567,
    "preview": "# Weather Plugin Example\n\nThis example demonstrates a WASM plugin with **network capability** and **resource provider** "
  },
  {
    "path": "examples/wasm-plugins/example-weather-plugin/asconfig.json",
    "chars": 499,
    "preview": "{\n  \"targets\": {\n    \"release\": {\n      \"outFile\": \"build/plugin.wasm\",\n      \"sourceMap\": false,\n      \"optimize\": true"
  },
  {
    "path": "examples/wasm-plugins/example-weather-plugin/assembly/index.ts",
    "chars": 15350,
    "preview": "/**\n * Weather Plugin Example for Signal K\n *\n * Demonstrates:\n * - Network capability by fetching weather data from Ope"
  },
  {
    "path": "examples/wasm-plugins/example-weather-plugin/package.json",
    "chars": 1122,
    "preview": "{\n  \"name\": \"@signalk/example-weather-plugin\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Example SignalK WASM plugin demon"
  },
  {
    "path": "examples/wasm-plugins/example-weather-provider/.gitignore",
    "chars": 102,
    "preview": "# Build artifacts\nbuild/\n*.wasm\n\n# Dependencies\nnode_modules/\npackage-lock.json\n\n# OS files\n.DS_Store\n"
  },
  {
    "path": "examples/wasm-plugins/example-weather-provider/.npmignore",
    "chars": 184,
    "preview": "# Source files - not needed for runtime\nassembly/\nasconfig.json\n\n# Dev/build artifacts\nnode_modules/\n*.tgz\n*.debug.wasm\n"
  },
  {
    "path": "examples/wasm-plugins/example-weather-provider/README.md",
    "chars": 5128,
    "preview": "# Weather Provider Plugin Example\n\nThis example demonstrates how to create a WASM plugin that integrates with Signal K's"
  },
  {
    "path": "examples/wasm-plugins/example-weather-provider/asconfig.json",
    "chars": 499,
    "preview": "{\n  \"targets\": {\n    \"release\": {\n      \"outFile\": \"build/plugin.wasm\",\n      \"sourceMap\": false,\n      \"optimize\": true"
  },
  {
    "path": "examples/wasm-plugins/example-weather-provider/assembly/index.ts",
    "chars": 15718,
    "preview": "/**\n * Weather Provider Plugin Example for Signal K\n *\n * Demonstrates:\n * - Weather Provider API integration (Signal K'"
  },
  {
    "path": "examples/wasm-plugins/example-weather-provider/package.json",
    "chars": 1077,
    "preview": "{\n  \"name\": \"@signalk/example-weather-provider\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Example SignalK WASM plugin dem"
  },
  {
    "path": "fly_io/cr_signalk_io/Dockerfile",
    "chars": 55,
    "preview": "FROM nginx:latest\nCOPY nginx.conf /etc/nginx/nginx.conf"
  },
  {
    "path": "fly_io/cr_signalk_io/fly.toml",
    "chars": 720,
    "preview": "# fly.toml file generated for cr-signalk-io on 2023-03-21T20:08:04+02:00\n\napp = \"cr-signalk-io\"\nkill_signal = \"SIGINT\"\nk"
  },
  {
    "path": "fly_io/cr_signalk_io/nginx.conf",
    "chars": 191,
    "preview": "events {\n  worker_connections  4096;  ## Default: 1024\n}\nhttp {\n  server {\n      listen 8080;\n      server_name cr.signa"
  },
  {
    "path": "fly_io/demo_signalk_org/Dockerfile",
    "chars": 262,
    "preview": "ARG SK_VERSION\nFROM signalk/signalk-server:${SK_VERSION}\nCOPY --chown=node:node --chmod=600 security.json /home/node/.si"
  },
  {
    "path": "fly_io/demo_signalk_org/fly.toml",
    "chars": 703,
    "preview": "# fly.toml file generated for demo-signalk-org on 2022-09-23T17:08:59+03:00\n\napp = \"demo-signalk-org\"\nkill_signal = \"SIG"
  },
  {
    "path": "fly_io/demo_signalk_org/security.json",
    "chars": 150,
    "preview": "{\n  \"allow_readonly\": true,\n  \"allowNewUserRegistration\": false,\n  \"allowDeviceAccessRequests\": false,\n  \"users\": [],\n  "
  },
  {
    "path": "index.js",
    "chars": 667,
    "preview": "/*\n * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl>\n * \n * Licensed under the Apache License, Version "
  },
  {
    "path": "kubernetes/README.md",
    "chars": 5043,
    "preview": "# General\n\nA simple manifest for deploying the Signal K Server is available in the `signalk-deployment.yaml` file. In ad"
  },
  {
    "path": "kubernetes/signalk-deployment.yaml",
    "chars": 1549,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: signalk\n  name: signalk\nspec:\n  externalTrafficPolicy: Cluster"
  },
  {
    "path": "kubernetes/signalk-ingress.yaml",
    "chars": 291,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: signalk\nspec:\n  rules:\n    - http:\n        paths:\n     "
  },
  {
    "path": "kubernetes.md",
    "chars": 2306,
    "preview": "# Running Signalk in Docker and kubernetes\n\nSignalk-server can run in Docker and kubernetes. The following steps provide"
  },
  {
    "path": "package.json",
    "chars": 6135,
    "preview": "{\n  \"name\": \"signalk-server\",\n  \"version\": \"2.26.0\",\n  \"description\": \"An implementation of a [Signal K](http://signalk."
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/.npmignore",
    "chars": 14,
    "preview": "node_modules\r\n"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/LICENSE",
    "chars": 11316,
    "preview": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licens"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/README.md",
    "chars": 915,
    "preview": "# Signal K AssemblyScript Plugin SDK\n\nBuild WASM plugins for Signal K Server using TypeScript-like syntax.\n\n## Features\n"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/asconfig.json",
    "chars": 500,
    "preview": "{\n  \"targets\": {\n    \"release\": {\n      \"outFile\": \"build/plugin.wasm\",\n      \"sourceMap\": false,\n      \"optimize\": true"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/assembly/api.ts",
    "chars": 6928,
    "preview": "/**\n * Signal K Server API functions for AssemblyScript plugins\n *\n * These functions provide the FFI bridge to the Sign"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/assembly/index.ts",
    "chars": 382,
    "preview": "/**\n * Signal K AssemblyScript Plugin SDK\n *\n * Provides TypeScript-like API for building WASM plugins\n */\n\n// Export al"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/assembly/network.ts",
    "chars": 1338,
    "preview": "/**\n * Network API for AssemblyScript plugins\n *\n * Provides capability checking for network access\n * Requires 'network"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/assembly/plugin.ts",
    "chars": 1227,
    "preview": "/**\n * Base Plugin class that all AssemblyScript plugins must extend\n */\n\n/**\n * Abstract base class for Signal K WASM p"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/assembly/resources.ts",
    "chars": 4409,
    "preview": "/**\n * Signal K Resource Provider API for AssemblyScript plugins\n *\n * Allows WASM plugins to act as resource providers "
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/assembly/signalk.ts",
    "chars": 3511,
    "preview": "/**\n * Signal K data model types for AssemblyScript\n */\n\n/**\n * Position with latitude and longitude\n */\nexport class Po"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/build/plugin.d.ts",
    "chars": 2717,
    "preview": "/** Exported memory */\nexport declare const memory: WebAssembly.Memory;\n/** assembly/signalk/NotificationState */\nexport"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/build/plugin.js",
    "chars": 8969,
    "preview": "async function instantiate(module, imports = {}) {\n  const adaptedImports = {\n    env: Object.assign(Object.create(globa"
  },
  {
    "path": "packages/assemblyscript-plugin-sdk/package.json",
    "chars": 1263,
    "preview": "{\n  \"name\": \"@signalk/assemblyscript-plugin-sdk\",\n  \"version\": \"0.2.0\",\n  \"description\": \"AssemblyScript SDK for develop"
  },
  {
    "path": "packages/resources-provider-plugin/.gitignore",
    "chars": 43,
    "preview": "/plugin\r\n/node_modules\r\npackage-lock.json\r\n"
  },
  {
    "path": "packages/resources-provider-plugin/.npmignore",
    "chars": 54,
    "preview": "package-lock.json\r\npackage.json\r\n/src\r\ntsconfig.json\r\n"
  },
  {
    "path": "packages/resources-provider-plugin/CHANGELOG.md",
    "chars": 1381,
    "preview": "# CHANGELOG: RESOURCES-PROVIDER\n\n**_Note: Can only be used with Signal K server version 2.0.0 or later._**\n\n---\n\n## v1.2"
  },
  {
    "path": "packages/resources-provider-plugin/LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "packages/resources-provider-plugin/README.md",
    "chars": 3128,
    "preview": "# Signal K Resources Provider Plugin:\n\n**Signal K server plugin that implements the Resource Provider API**.\n\n_Note: Thi"
  },
  {
    "path": "packages/resources-provider-plugin/package.json",
    "chars": 1127,
    "preview": "{\n  \"name\": \"@signalk/resources-provider\",\n  \"version\": \"1.5.1\",\n  \"description\": \"Resources provider plugin for Signal "
  },
  {
    "path": "packages/resources-provider-plugin/src/@types/geojson-validation.d.ts",
    "chars": 36,
    "preview": "declare module 'geojson-validation'\n"
  },
  {
    "path": "packages/resources-provider-plugin/src/index.ts",
    "chars": 12094,
    "preview": "import {\n  Plugin,\n  ServerAPI,\n  ResourceProviderRegistry,\n  SIGNALKRESOURCETYPES\n} from '@signalk/server-api'\n\nimport "
  },
  {
    "path": "packages/resources-provider-plugin/src/openApi.json",
    "chars": 6253,
    "preview": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"version\": \"2.0.0\",\n    \"title\": \"Resources Provider (Built-in)\",\n    \"termsOfSe"
  },
  {
    "path": "packages/resources-provider-plugin/src/types/index.ts",
    "chars": 24,
    "preview": "export * from './store'\n"
  },
  {
    "path": "packages/resources-provider-plugin/src/types/store.ts",
    "chars": 497,
    "preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\n// ** Resource Store Interface\nexport interface IResourceStore {"
  },
  {
    "path": "packages/resources-provider-plugin/tsconfig.json",
    "chars": 209,
    "preview": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"allowJs\": false\n  },\n  \"include\": [\"./src/**/*\", "
  },
  {
    "path": "packages/server-admin-ui/.gitignore",
    "chars": 267,
    "preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\npackage-lock.json"
  },
  {
    "path": "packages/server-admin-ui/.npmignore",
    "chars": 717,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
  },
  {
    "path": "packages/server-admin-ui/README.md",
    "chars": 1052,
    "preview": "# @signalk/server-admin-ui\n\nAdmin interface for the [Signal K](http://signalk.org) [Node Server](https://github.com/Sign"
  },
  {
    "path": "packages/server-admin-ui/index.html",
    "chars": 615,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"I"
  },
  {
    "path": "packages/server-admin-ui/package.json",
    "chars": 3061,
    "preview": "{\n  \"name\": \"@signalk/server-admin-ui\",\n  \"version\": \"2.26.0\",\n  \"description\": \"Signal K server admin webapp\",\n  \"repos"
  },
  {
    "path": "packages/server-admin-ui/public_src/index.html",
    "chars": 1718,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"I"
  },
  {
    "path": "packages/server-admin-ui/scss/_bootstrap-variables.scss",
    "chars": 1866,
    "preview": "// Bootstrap overrides\n\n//\n// Color system\n//\n\n$white: #fff;\n$gray-100: #f0f3f5;\n$gray-200: #c2cfd6;\n$gray-300: #a4b7c1;"
  },
  {
    "path": "packages/server-admin-ui/scss/_core-variables.scss",
    "chars": 18,
    "preview": "// core overrides\n"
  },
  {
    "path": "packages/server-admin-ui/scss/_custom.scss",
    "chars": 6004,
    "preview": "// Here you can add other styles\n\n// Navbar toggler - remove border to match CoreUI v1 styling\n.app-header .navbar-toggl"
  },
  {
    "path": "packages/server-admin-ui/scss/core/_animate.scss",
    "chars": 326,
    "preview": "// scss-lint:disable all\n.animated {\n  animation-duration: 1s;\n  // animation-fill-mode: both;\n}\n\n.animated.infinite {\n "
  },
  {
    "path": "packages/server-admin-ui/scss/core/_aside.scss",
    "chars": 1577,
    "preview": "@use 'sass:color';\n\n.aside-menu {\n  z-index: $zindex-sticky - 1;\n  width: $aside-menu-width;\n  color: $aside-menu-color;"
  },
  {
    "path": "packages/server-admin-ui/scss/core/_avatars.scss",
    "chars": 792,
    "preview": ".img-avatar {\n  border-radius: 50em;\n}\n\n.avatar {\n  $width: 36px;\n  $status-width: 10px;\n  @include avatar($width, $stat"
  },
  {
    "path": "packages/server-admin-ui/scss/core/_badge.scss",
    "chars": 166,
    "preview": "// Bootstrap 5: .badge-pill is replaced by .rounded-pill\n// Keeping for backwards compatibility\n.badge-pill {\n  border-r"
  },
  {
    "path": "packages/server-admin-ui/scss/core/_breadcrumb-menu.scss",
    "chars": 503,
    "preview": ".breadcrumb-menu {\n  margin-left: auto;\n\n  &::before {\n    display: none;\n  }\n\n  .btn-group {\n    vertical-align: top;\n "
  },
  {
    "path": "packages/server-admin-ui/scss/core/_breadcrumb.scss",
    "chars": 79,
    "preview": ".breadcrumb {\n  position: relative;\n  @include borders($breadcrumb-borders);\n}\n"
  },
  {
    "path": "packages/server-admin-ui/scss/core/_buttons.scss",
    "chars": 9525,
    "preview": "@use 'sass:color';\n\nbutton {\n  cursor: pointer;\n}\n\n.btn {\n  .badge {\n    position: absolute;\n    top: 2px;\n    right: 6p"
  },
  {
    "path": "packages/server-admin-ui/scss/core/_callout.scss",
    "chars": 825,
    "preview": ".callout {\n  position: relative;\n  padding: 0 $spacer;\n  margin: $spacer 0;\n  border: 0 solid $border-color;\n  border-le"
  }
]

// ... and 487 more files (download for full content)

About this extraction

This page contains the full source code of the SignalK/signalk-server GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 687 files (9.1 MB), approximately 2.4M tokens, and a symbol index with 2127 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!