signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
207 lines (194 loc) • 42.3 kB
HTML
<html class="default" lang="en" data-base="../../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>WebApps | Signal K</title><meta name="description" content="Documentation for Signal K"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../../assets/style.css?cache=1783009712609"/><link rel="stylesheet" href="../../assets/highlight.css?cache=1783009712609"/><script defer src="../../assets/main.js?cache=1783009712609"></script><script async src="../../assets/icons.js?cache=1783009712609" id="tsd-icons-script"></script><script async src="../../assets/search.js?cache=1783009712609" id="tsd-search-script"></script><script async src="../../assets/navigation.js?cache=1783009712609" id="tsd-nav-script"></script><script async src="../../assets/hierarchy.js?cache=1783009712609" id="tsd-hierarchy-script"></script><link rel="stylesheet" href="../../assets/theme.css"/><script>if(window.self!==window.top){document.documentElement.classList.add('embedded')}</script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../../index.html" class="title"><img class="title-logo" src="../../assets/logo.svg" alt="Signal K"/><svg class="title-home-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="Documentation home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg></a><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../../assets/icons.svg#icon-search"></use></svg><span class="visible@s">Search</span></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><div id="tsd-toolbar-links"><a href="https://discord.gg/uuZrwz4dCS" target="_blank" rel="noopener" class="toolbar-icon visible@s" aria-label="Discord"><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M524.5 69.8a1.5 1.5 0 0 0 -.8-.7A485.1 485.1 0 0 0 404.1 32a1.8 1.8 0 0 0 -1.9 .9 337.5 337.5 0 0 0 -14.9 30.6 447.8 447.8 0 0 0 -134.4 0 309.5 309.5 0 0 0 -15.1-30.6 1.9 1.9 0 0 0 -1.9-.9A483.7 483.7 0 0 0 116.1 69.1a1.7 1.7 0 0 0 -.8 .7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 0 0 .8 1.4A487.7 487.7 0 0 0 176 479.9a1.9 1.9 0 0 0 2.1-.7A348.2 348.2 0 0 0 208.1 430.4a1.9 1.9 0 0 0 -1-2.6 321.2 321.2 0 0 1 -45.9-21.9 1.9 1.9 0 0 1 -.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8 0 0 1 1.9-.3c96.2 43.9 200.4 43.9 295.5 0a1.8 1.8 0 0 1 1.9 .2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9 0 0 1 -.2 3.1 301.4 301.4 0 0 1 -45.9 21.8 1.9 1.9 0 0 0 -1 2.6 391.1 391.1 0 0 0 30 48.8 1.9 1.9 0 0 0 2.1 .7A486 486 0 0 0 610.7 405.7a1.9 1.9 0 0 0 .8-1.4C623.7 277.6 590.9 167.5 524.5 69.8zM222.5 337.6c-29 0-52.8-26.6-52.8-59.2S193.1 219.1 222.5 219.1c29.7 0 53.3 26.8 52.8 59.2C275.3 311 251.9 337.6 222.5 337.6zm195.4 0c-29 0-52.8-26.6-52.8-59.2S388.4 219.1 417.9 219.1c29.7 0 53.3 26.8 52.8 59.2C470.7 311 447.5 337.6 417.9 337.6z"></path></svg></a><a href="https://github.com/SignalK/signalk-server" target="_blank" rel="noopener" class="toolbar-icon visible@s" aria-label="Discord"><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg></a><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="../../Developing.html">Developing</a></li><li><a href="../Plugins.html">Plugins</a></li><li><a href="" aria-current="page">WebApps</a></li></ul></div><div class="tsd-panel tsd-typography"><h1 id="webapps-and-components" class="tsd-anchor-link">WebApps and Components<a href="#webapps-and-components" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h1>
<p>Signal K Server provides the following ways to add web-based user interfaces to enhance functionality and usability:</p>
<ol>
<li>
<p><strong>Standalone WebApps</strong> are web applications that when launched, the server Admin UI disappears and the webapp controls the whole page (browser window / tab).</p>
</li>
<li>
<p><strong>Embedded WebApps</strong> are web applications that when launched, are <strong>embedded in the server Admin UI</strong>, leaving the toolbar and menu available to the user.</p>
</li>
<li>
<p><strong>Embedded Plugin Configuration Forms</strong> are forms provided by a plugin that the server embeds within the <em>Plugin Config</em> screen to replace the generic form rendered using the plugin <em>configuration schema</em>. This allows a richer set of controls to be provided for the user to configure the plugin compared to the more generice server generated form provides.
<img src="../../media/calibration.png" alt="calibration" title="Calibration plugin configuration form"></p>
</li>
<li>
<p><strong>Embedded Components</strong> are individual UI components provided by a plugin or a webapp. They are listed in the <em>Addons</em> section at the bottom of the <em>Webapps</em> page of the Admin UI. More a concept than a fully implemented feature at this stage, the idea is to allow a plugin to add individual components to different parts of the server UI.</p>
</li>
</ol>
<p>All Plugins, WebApps and Components can be installed via the <em>Appstore</em>.</p>
<h2 id="webapp-structure" class="tsd-anchor-link">WebApp Structure<a href="#webapp-structure" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>All WebApps (like plugins) are installed with <code>npm</code>, either from the npm registry or from your own Github repository. Only WebApps that are relevant for all users should be published to <code>npm</code> to be made available in the <em>Appstore</em> of all Signal K Servers.</p>
<p><em>Note: Private plugins need not be published to <code>npm</code> - see the documentation for <a href="https://docs.npmjs.com/cli/v6/commands/npm-install">npm install</a> for details.</em></p>
<p>The basic structure of a webapp is:</p>
<ul>
<li>A folder named <code>public</code> that contains the html, JavaScript and resource files such as images, fonts and style sheets. This folder is automatically mounted by the server so that the webapp is available after installation and the server restarted.</li>
<li><code>package.json</code> containing special keywords that classifies the webapp:
<ul>
<li><code>signalk-webapp</code> - standalone webapp</li>
<li><code>signalk-embeddable-webapp</code> - embeddable webapp</li>
<li><code>signalk-plugin-configurator</code> - plugin configuration form</li>
</ul>
</li>
</ul>
<p>This structure is all that is needed for a standalone webapp.</p>
<p>You can also include the following section in <code>package.json</code> to control how your webapp appears in the <em>Webapps</em> list:</p>
<pre><code class="JSON"><span class="hl-0"> </span><span class="hl-4">"signalk"</span><span class="hl-0">: {</span><br/><span class="hl-0"> </span><span class="hl-3">"appIcon"</span><span class="hl-0">: </span><span class="hl-4">"./assets/icons/icon-72x72.png"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"displayName"</span><span class="hl-0">: </span><span class="hl-4">"Freeboard-SK"</span><br/><span class="hl-0"> },</span>
</code><button type="button">Copy</button></pre>
<p>where:</p>
<ul>
<li><code>appIcon</code> is the path (relative to the <code>public</code> directory) to an image within the package to display in the webapp list. The image should be at least 72x72 pixels in size.</li>
<li><code>displayName</code> is the text you want to appear as the name in the webapp list. <em>(By default the _name</em> attribute in the <code>package.json</code> is used.)_. Displayname is also used in an automatic redirect from the root of the server: if you have a webapp with displayName <code>foo</code> and you access it using for example the url <a href="http://foo.bar.org:3000">http://foo.bar.org:3000</a> the first part of the hostname matches the webapp's displayName and you will be redirected to it instead of the default landingPage, the Admin webapp. With this mechanism you can add easy to access DNS names to each webapp, including .local names.</li>
</ul>
<p>See also <a href="../../Developing.html#offline-use">Working Offline</a>.</p>
<h2 id="application-data-storing-webapp-data-on-the-server" class="tsd-anchor-link">Application Data: Storing Webapp Data on the Server<a href="#application-data-storing-webapp-data-on-the-server" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>Application Data is only supported if security is turned on. It supports two namespaces, one for <em>global data</em> and one for <em>user specific data</em>. For example, a client might want to store boat specific gauge configuration globally so that other users have access to it. Otherwise, it could use the user area to store user specific preferences.</p>
<p>The data is structured and manipulated in JSON format.</p>
<p>Global storage: <code>/signalk/v1/applicationData/global/:appid/:version</code>
User storage: <code>/signalk/v1/applicationData/user/:appid/:version</code></p>
<p>There are two ways to update or add stored data:</p>
<ul>
<li>You can POST any json data to any path:</li>
</ul>
<pre><code><span class="hl-2">POST</span><span class="hl-0"> </span><span class="hl-1">/</span><span class="hl-0">signalk</span><span class="hl-1">/</span><span class="hl-0">v1</span><span class="hl-1">/</span><span class="hl-0">applicationData</span><span class="hl-1">/</span><span class="hl-0">user</span><span class="hl-1">/</span><span class="hl-0">my</span><span class="hl-1">-</span><span class="hl-0">application</span><span class="hl-1">/</span><span class="hl-2">1.0</span><span class="hl-1">/</span><span class="hl-0">unitPreferences</span><br/><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-4">"shortDistance"</span><span class="hl-0">: </span><span class="hl-4">"m"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-4">"longDistance"</span><span class="hl-0">: </span><span class="hl-4">"km"</span><br/><span class="hl-0">}</span>
</code><button>Copy</button></pre>
<ul>
<li>You can also use json patch format (<a href="http://jsonpatch.com">http://jsonpatch.com</a>):</li>
</ul>
<pre><code><span class="hl-2">POST</span><span class="hl-0"> </span><span class="hl-1">/</span><span class="hl-0">signalk</span><span class="hl-1">/</span><span class="hl-0">v1</span><span class="hl-1">/</span><span class="hl-0">applicationData</span><span class="hl-1">/</span><span class="hl-0">user</span><span class="hl-1">/</span><span class="hl-0">my</span><span class="hl-1">-</span><span class="hl-0">application</span><span class="hl-1">/</span><span class="hl-2">1.0</span><br/><span class="hl-0">[</span><br/><span class="hl-0"> { </span><span class="hl-4">"op"</span><span class="hl-0">: </span><span class="hl-4">"add"</span><span class="hl-0">, </span><span class="hl-4">"path"</span><span class="hl-0">: </span><span class="hl-4">"/unitPreferences"</span><span class="hl-0">, </span><span class="hl-4">"value"</span><span class="hl-0">: { </span><span class="hl-4">"shortDistace"</span><span class="hl-0">: </span><span class="hl-4">"m"</span><span class="hl-0"> } },</span><br/><span class="hl-0"> { </span><span class="hl-4">"op"</span><span class="hl-0">: </span><span class="hl-4">"add"</span><span class="hl-0">, </span><span class="hl-4">"path"</span><span class="hl-0">: </span><span class="hl-4">"/unitPreferences/longDistance"</span><span class="hl-0">, </span><span class="hl-4">"value"</span><span class="hl-0">: </span><span class="hl-4">"km"</span><span class="hl-0">}</span><br/><span class="hl-0">]</span>
</code><button>Copy</button></pre>
<p>Use an HTTP GET request to retrieve data from the server:</p>
<p><code>GET /signalk/v1/applicationData/user/my-application/1.0/unitPreferences/shortDistance</code></p>
<p>You can just GET the list of keys:</p>
<pre><code><span class="hl-2">GET</span><span class="hl-0"> </span><span class="hl-1">/</span><span class="hl-0">signalk</span><span class="hl-1">/</span><span class="hl-0">v1</span><span class="hl-1">/</span><span class="hl-0">applicationData</span><span class="hl-1">/</span><span class="hl-0">user</span><span class="hl-1">/</span><span class="hl-0">my</span><span class="hl-1">-</span><span class="hl-0">application</span><span class="hl-1">/</span><span class="hl-2">1.0</span><span class="hl-1">/</span><span class="hl-0">unitPreferences</span><span class="hl-1">?</span><span class="hl-0">keys</span><span class="hl-1">=</span><span class="hl-2">true</span><br/><span class="hl-0">[ </span><span class="hl-4">"longDistance"</span><span class="hl-0">, </span><span class="hl-4">"shortDistance"</span><span class="hl-0">]</span>
</code><button>Copy</button></pre>
<p>You get can a list of available versions:</p>
<pre><code><span class="hl-2">GET</span><span class="hl-0"> </span><span class="hl-1">/</span><span class="hl-0">signalk</span><span class="hl-1">/</span><span class="hl-0">v1</span><span class="hl-1">/</span><span class="hl-0">applicationData</span><span class="hl-1">/</span><span class="hl-0">user</span><span class="hl-1">/</span><span class="hl-0">my</span><span class="hl-1">-</span><span class="hl-0">application</span><br/><span class="hl-0">[ </span><span class="hl-4">"1.0"</span><span class="hl-0">, </span><span class="hl-4">"1.1"</span><span class="hl-0">]</span>
</code><button>Copy</button></pre>
<h2 id="discovering-server-features" class="tsd-anchor-link">Discovering Server Features<a href="#discovering-server-features" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>To assist in tailoring a WebApps UI, it can "discover" the features supported by the server by sending a request to <code>/signalk/v2/features</code>.</p>
<p>The response wil contain an object detailing the available APIs and Plugins.</p>
<p>You can use the <code>enabled</code> parameter to specify to only return enabled or disabled features.</p>
<p>To list only enabled features:
<code>/signalk/v2/features?enable=1</code></p>
<p>To list only disabled features:
<code>/signalk/v2/features?enable=0</code></p>
<p><em>Example response:</em></p>
<pre><code class="JSON"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-3">"apis"</span><span class="hl-0">: [</span><br/><span class="hl-0"> </span><span class="hl-4">"resources"</span><span class="hl-0">,</span><span class="hl-4">"course"</span><br/><span class="hl-0"> ],</span><br/><span class="hl-0"> </span><span class="hl-3">"plugins"</span><span class="hl-0">: [</span><br/><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-3">"id"</span><span class="hl-0">: </span><span class="hl-4">"anchoralarm"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"name"</span><span class="hl-0">: </span><span class="hl-4">"Anchor Alarm"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"version"</span><span class="hl-0">: </span><span class="hl-4">"1.13.0"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"enabled"</span><span class="hl-0">: </span><span class="hl-2">true</span><br/><span class="hl-0"> },</span><br/><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-3">"id"</span><span class="hl-0">: </span><span class="hl-4">"autopilot"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"name"</span><span class="hl-0">: </span><span class="hl-4">"Autopilot Control"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"version"</span><span class="hl-0">: </span><span class="hl-4">"1.4.0"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"enabled"</span><span class="hl-0">: </span><span class="hl-2">false</span><br/><span class="hl-0"> },</span><br/><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-3">"id"</span><span class="hl-0">: </span><span class="hl-4">"sk-to-nmea2000"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"name"</span><span class="hl-0">: </span><span class="hl-4">"Signal K to NMEA 2000"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"version"</span><span class="hl-0">: </span><span class="hl-4">"2.17.0"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"enabled"</span><span class="hl-0">: </span><span class="hl-2">false</span><br/><span class="hl-0"> },</span><br/><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-3">"id"</span><span class="hl-0">: </span><span class="hl-4">"udp-nmea-sender"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"name"</span><span class="hl-0">: </span><span class="hl-4">"UDP NMEA0183 Sender"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"version"</span><span class="hl-0">: </span><span class="hl-4">"2.0.0"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"enabled"</span><span class="hl-0">: </span><span class="hl-2">false</span><br/><span class="hl-0"> }</span><br/><span class="hl-0"> ]</span><br/><span class="hl-0">}</span>
</code><button type="button">Copy</button></pre>
<h2 id="inheriting-the-admin-uis-styling" class="tsd-anchor-link">Inheriting the Admin UI's Styling<a href="#inheriting-the-admin-uis-styling" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>Standalone webapps that want to match the Admin UI's look-and-feel can load its compiled stylesheet. The Admin UI is built with Vite and emits content-hashed filenames (e.g. <code>style-__hQMaH5.css</code>) that change on every rebuild, so the path can't be hardcoded.</p>
<p><code>GET /admin/.vite/manifest.json</code> returns the standard <a href="https://vite.dev/config/build-options#build-manifest">Vite manifest</a>:</p>
<pre><code class="json"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-3">"index.html"</span><span class="hl-0">: {</span><br/><span class="hl-0"> </span><span class="hl-3">"file"</span><span class="hl-0">: </span><span class="hl-4">"assets/index-BzRlmq2e.js"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"name"</span><span class="hl-0">: </span><span class="hl-4">"index"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"src"</span><span class="hl-0">: </span><span class="hl-4">"index.html"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"isEntry"</span><span class="hl-0">: </span><span class="hl-2">true</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-3">"css"</span><span class="hl-0">: [</span><span class="hl-4">"assets/style-__hQMaH5.css"</span><span class="hl-0">]</span><br/><span class="hl-0"> }</span><br/><span class="hl-0">}</span>
</code><button type="button">Copy</button></pre>
<p>The webapp fetches the manifest at load time, collects CSS files from all entry points, then injects the stylesheet URLs into its own <code><head></code>:</p>
<pre><code class="javascript"><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-2">res</span><span class="hl-0"> </span><span class="hl-1">=</span><span class="hl-0"> </span><span class="hl-1">await</span><span class="hl-0"> </span><span class="hl-7">fetch</span><span class="hl-0">(</span><span class="hl-4">'/admin/.vite/manifest.json'</span><span class="hl-0">)</span><br/><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-2">manifest</span><span class="hl-0"> </span><span class="hl-1">=</span><span class="hl-0"> </span><span class="hl-1">await</span><span class="hl-0"> res.</span><span class="hl-7">json</span><span class="hl-0">()</span><br/><span class="hl-1">for</span><span class="hl-0"> (</span><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-2">entry</span><span class="hl-0"> </span><span class="hl-1">of</span><span class="hl-0"> Object.</span><span class="hl-7">values</span><span class="hl-0">(manifest)) {</span><br/><span class="hl-0"> </span><span class="hl-1">if</span><span class="hl-0"> (</span><span class="hl-1">!</span><span class="hl-0">entry.isEntry) </span><span class="hl-1">continue</span><br/><span class="hl-0"> </span><span class="hl-1">for</span><span class="hl-0"> (</span><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-2">css</span><span class="hl-0"> </span><span class="hl-1">of</span><span class="hl-0"> entry.css </span><span class="hl-1">??</span><span class="hl-0"> []) {</span><br/><span class="hl-0"> </span><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-2">link</span><span class="hl-0"> </span><span class="hl-1">=</span><span class="hl-0"> document.</span><span class="hl-7">createElement</span><span class="hl-0">(</span><span class="hl-4">'link'</span><span class="hl-0">)</span><br/><span class="hl-0"> link.rel </span><span class="hl-1">=</span><span class="hl-0"> </span><span class="hl-4">'stylesheet'</span><br/><span class="hl-0"> link.href </span><span class="hl-1">=</span><span class="hl-0"> </span><span class="hl-4">'/admin/'</span><span class="hl-0"> </span><span class="hl-1">+</span><span class="hl-0"> css</span><br/><span class="hl-0"> document.head.</span><span class="hl-7">appendChild</span><span class="hl-0">(link)</span><br/><span class="hl-0"> }</span><br/><span class="hl-0">}</span>
</code><button type="button">Copy</button></pre>
<h2 id="embedded-components-and-admin-ui-server-interfaces" class="tsd-anchor-link">Embedded Components and Admin UI / Server interfaces<a href="#embedded-components-and-admin-ui-server-interfaces" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>Embedded components are implemented using <a href="https://module-federation.io/">Module Federation</a> and <a href="https://react.dev/reference/react/lazy">React Code Splitting</a>.</p>
<p><em>Note: There is no keyword for a module that provides only embedded components, use <code>signalk-webapp</code> instead.</em></p>
<p>You need to configure your build tool (Webpack or Vite) to create the necessary code for federation and expose the component with fixed names:</p>
<ul>
<li>embeddable webapp: <code>./AppPanel</code></li>
<li>plugin configuration form: <code>./PluginConfigurationPanel</code></li>
<li>embedded component: <code>./AddonPanel</code></li>
</ul>
<p>The exposed modules need to <code>export default</code> a React component. Functional components with hooks are recommended. The server dependencies like <code>reactstrap</code> can and should be used. Add <code>@signalk/server-admin-ui-dependencies</code> as a dependency to the webapp, it defines the dependencies used by the server admin UI.</p>
<h3 id="webpack-var-library" class="tsd-anchor-link">Webpack (var library)<a href="#webpack-var-library" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h3>
<p>With Webpack's ModuleFederationPlugin, use <code>library.type: 'var'</code> with a safe module name derived from the package name:</p>
<pre><code class="javascript"><span class="hl-5">library</span><span class="hl-0">: { </span><span class="hl-5">type</span><span class="hl-0">: </span><span class="hl-4">'var'</span><span class="hl-0">, </span><span class="hl-5">name</span><span class="hl-0">: packageJson.name.</span><span class="hl-7">replace</span><span class="hl-0">(</span><span class="hl-4">/</span><span class="hl-2">[-@/]</span><span class="hl-4">/</span><span class="hl-1">g</span><span class="hl-0">, </span><span class="hl-4">'_'</span><span class="hl-0">) },</span>
</code><button type="button">Copy</button></pre>
<p>The server loads these via a classic <code><script></code> tag which places the container on the <code>window</code> object.</p>
<h3 id="vite-esm-bundlers" class="tsd-anchor-link">Vite / ESM bundlers<a href="#vite-esm-bundlers" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h3>
<p>The server also supports ESM containers produced by Vite, Rollup, esbuild, or any bundler that outputs <code>export { init, get }</code>. Set <code>"type": "module"</code> in the plugin's <code>package.json</code> and the server emits <code><script type="module"></code> for the container; the Admin UI loads it via dynamic <code>import()</code> when the expected name is not present on <code>window</code>.</p>
<p>If the plugin's server-side code remains CommonJS (<code>module.exports</code>), give <code>"main"</code> a <code>.cjs</code> extension so Node.js still loads it as CommonJS under the module-typed package.</p>
<h3 id="react-version-compatibility" class="tsd-anchor-link">React Version Compatibility<a href="#react-version-compatibility" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h3>
<p>The Admin UI uses <strong>React 19</strong> with functional components and hooks. Your plugin must use the host's React instance — bundling a second copy yields <code>Cannot read properties of null (reading 'useState')</code> when the plugin's hooks read a dispatcher that the host activated on its React, not the plugin's.</p>
<h4 id="sharing-react-with-the-host" class="tsd-anchor-link">Sharing React with the host<a href="#sharing-react-with-the-host" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h4>
<p><strong>Webpack (<code>var</code> library)</strong> — webpack-MF's runtime hooks into the host's share scope automatically when you declare React as a singleton:</p>
<pre><code class="javascript"><span class="hl-5">shared</span><span class="hl-0">: {</span><br/><span class="hl-0"> </span><span class="hl-5">react</span><span class="hl-0">: { </span><span class="hl-5">singleton</span><span class="hl-0">: </span><span class="hl-2">true</span><span class="hl-0">, </span><span class="hl-5">requiredVersion</span><span class="hl-0">: </span><span class="hl-4">'^19'</span><span class="hl-0"> },</span><br/><span class="hl-0"> </span><span class="hl-4">'react-dom'</span><span class="hl-0">: { singleton: </span><span class="hl-2">true</span><span class="hl-0">, requiredVersion: </span><span class="hl-4">'^19'</span><span class="hl-0"> }</span><br/><span class="hl-0">}</span>
</code><button type="button">Copy</button></pre>
<p><strong>ESM bundlers</strong> — the host exposes the following on <code>window</code> before render:</p>
<table>
<thead>
<tr>
<th>Global</th>
<th>Module</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>window.__SK_REACT__</code></td>
<td><code>react</code></td>
</tr>
<tr>
<td><code>window.__SK_REACT_DOM__</code></td>
<td><code>react-dom</code></td>
</tr>
<tr>
<td><code>window.__SK_REACT_DOM_CLIENT__</code></td>
<td><code>react-dom/client</code></td>
</tr>
<tr>
<td><code>window.__SK_REACT_JSX_RUNTIME__</code></td>
<td><code>react/jsx-runtime</code></td>
</tr>
</tbody>
</table>
<p>Resolve the plugin's <code>react</code>/<code>react-dom</code>/<code>react/jsx-runtime</code> imports to small shims that re-export from these globals (e.g. via Vite's <code>resolve.alias</code> to a per-module file that does <code>const host = globalThis.__SK_REACT__; export default host.default ?? host; export const useState = host.useState; ...</code>). With imports redirected to the host, no federation <code>shared</code> declaration is needed.</p>
<p>For a working <code>@module-federation/vite</code> setup including the alias wiring and shim files, see the <a href="https://github.com/dirkwa/signalk-container">signalk-container plugin</a>.</p>
<p>See the Calibration plugin for an existing webpack-based example to start from. It is probably easier to start with an existing plugin and modify it to suit your needs. Don't forget to change the module id and name in package.json!</p>
<h2 id="webapp-component-and-admin-ui--server-interfaces" class="tsd-anchor-link">WebApp / Component and Admin UI / Server interfaces<a href="#webapp-component-and-admin-ui--server-interfaces" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>Standalone WebApps can use the server's APIs <em>(Signal K http and WebSocket APIs as well as any server specific endpoints)</em> but they need to implement everything else themselves.</p>
<p>Embedded WebApps, Components and Plugin Configuration Forms work inside the Admin UI, so they can interact with both the Admin UI and the server using APIs exposed by the Admin UI as component properties.</p>
<p>Embedded webapp properties:</p>
<ul>
<li>access to the login status of the browser user</li>
<li>ability to render Login form instead of the webapp content</li>
<li>getting and setting application data</li>
<li>opening an automatically reconnecting WebSocket connection to the server</li>
<li>getting Signal K data via <code>get</code></li>
<li><a href="https://github.com/SignalK/signalk-server/blob/master/packages/server-admin-ui/src/views/Webapps/Embedded.tsx">Embedded</a></li>
</ul>
<p>PluginConfigurationForm properties:</p>
<ul>
<li><code>configuration</code> : the configuration data of the plugin</li>
<li><code>save</code>: function to save the configuration data</li>
<li><a href="https://github.com/SignalK/signalk-server/blob/master/packages/server-admin-ui/src/views/Configuration/EmbeddedPluginConfigurationForm.tsx">EmbeddedPluginConfigurationForm</a></li>
</ul>
<p><strong><em>Note: The documentation regarding embedded WebApps and Components provided at this time is rudimentary and should be considered under development as the concept is evolving.</em></strong></p>
<h2 id="authentication-and-session-management" class="tsd-anchor-link">Authentication and Session Management<a href="#authentication-and-session-management" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h2>
<p>Per <a href="https://signalk.org/specification/1.7.0/doc/security.html#authentication-via-http">the specification</a> the server provides the endpoint <code>/signalk/v1/auth/login</code> for logging in. A successful request will</p>
<ul>
<li>set an authentication cookie</li>
<li>return an authentication token</li>
</ul>
<p>For <strong>cookie based, shared sessions</strong> all a webapp needs to do is use <code>credentials: "include"</code> when making api calls with <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included">fetch</a>. Cookies are included automatically in the initial WebSocket opening HTTP request, so the same works automatically for WebSocket connections.</p>
<p>The session cookie's value is the same as the token value: it is a JWT token that includes a validity period and is signed by the server. The server is stateless: JWT is verified for each request for a valid signature and time. Validity period is governed by server's security <code>expires</code> configuration value that can be changed in Admin UI's Security section. The server uses a sliding session window: when a token is past the midpoint of its lifetime, the server silently issues a fresh token on the next HTTP request, so active users are never logged out.</p>
<p>The login endpoint has an optional <code>rememberMe</code> request parameter. By default, without <code>rememberMe</code> set to true, the cookie is erased on browser restarts per standard browser behavior. When true the response's set cookie header includes MaxAge value based on the server's <code>expires</code> value. This makes the cookie persist over browser restarts. The <code>rememberMe</code> flag is preserved in refreshed tokens.</p>
<p>As the cookie is set to be <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security"><code>HttpOnly</code></a> webapp JavaScript has no access to it. Including it in server requests and persisting its value is managed by the browser, governed by the <code>Set-Cookie</code> headers sent by the server.</p>
<p>Additionally the server sets cookie <code>skLoginInfo</code> when the user logs in and removes it when the user logs out. A webapp can poll for changes of this cookie to be notified of the browser's cookie based login status.</p>
<h3 id="redirecting-unauthenticated-users-to-login" class="tsd-anchor-link">Redirecting unauthenticated users to login<a href="#redirecting-unauthenticated-users-to-login" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../../assets/icons.svg#icon-anchor"></use></svg></a></h3>
<p>A webapp that requires an authenticated user can hand off sign-in to the server. Send the browser to the admin UI's login page with a <code>redirect</code> query parameter pointing back at the webapp:</p>
<pre><code class="js"><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-2">here</span><span class="hl-0"> </span><span class="hl-1">=</span><br/><span class="hl-0"> window.location.pathname </span><span class="hl-1">+</span><span class="hl-0"> window.location.search </span><span class="hl-1">+</span><span class="hl-0"> window.location.hash</span><br/><span class="hl-0">window.location.href </span><span class="hl-1">=</span><span class="hl-0"> </span><span class="hl-4">'/admin/#/login?redirect='</span><span class="hl-0"> </span><span class="hl-1">+</span><span class="hl-0"> </span><span class="hl-7">encodeURIComponent</span><span class="hl-0">(here)</span>
</code><button type="button">Copy</button></pre>
<p>After a successful form login or OIDC sign-in the browser is sent back to that URL. The session cookie is in place by then, so the webapp's subsequent requests are authenticated.</p>
<p>The <code>redirect</code> value must be a same-origin relative URL: it must start with <code>/</code> (but not <code>//</code>), contain no backslashes or control characters, and not point at the admin login route itself. Invalid values are silently ignored and the user lands on the dashboard after login.</p>
<p>For OIDC-only deployments, a webapp can skip the admin UI and link directly to the OIDC endpoint, which accepts the same parameter:</p>
<pre><code class="text">/signalk/v1/auth/oidc/login?redirect=/your/path
</code><button type="button">Copy</button></pre>
<p>For <strong>token based sessions</strong> a webapp may manage the authentication token itself. It must include it explicitly in fetch call headers.
As JavaScript has no access to headers but cookies are included automatically by browsers when opening WebSocket connections the server will use the server-set, HttpOnly cookie. Normally browsers do not allow shadowing the server-set cookie with a new value. The only option for WebSocket connections is using a query parameter to override the cookie with a token.</p>
<p>The order that the server uses for finding the JWT token is</p>
<ol>
<li>query parameter <code>token</code></li>
<li>request header <code>authorization</code></li>
<li>authorization cookie (name managed by the server)</li>
</ol>
<p>Token-based session management is currently discouraged, because it may result in <strong>session confusion</strong>: all login calls set the shared session cookie for all webapps that are using cookie based, shared sessions.</p>
<p>Each webapp acting separately, managing its authentication token independently, means that</p>
<ul>
<li>each application needs to implement token management separately so that closing and reopening the webapp during a browser session does not require the user to reauthenticate</li>
<li>when navigating between the different webapps the user needs to authenticate to each one separately</li>
</ul>
<p>The server's Admin UI is a regular webapp using cookie based sessions, there is no separate authentication mechanism.</p>
</div></div><div class="col-sidebar"><div class="page-menu"><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../../assets/icons.svg#icon-chevronDown"></use></svg><h3>On This Page</h3></summary><div class="tsd-accordion-details"><a href="#webapp-structure"><span>Web<wbr/>App <wbr/>Structure</span></a><a href="#application-data-storing-webapp-data-on-the-server"><span>Application <wbr/>Data: <wbr/>Storing <wbr/>Webapp <wbr/>Data on the <wbr/>Server</span></a><a href="#discovering-server-features"><span>Discovering <wbr/>Server <wbr/>Features</span></a><a href="#inheriting-the-admin-uis-styling"><span>Inheriting the <wbr/>Admin <wbr/>UI's <wbr/>Styling</span></a><a href="#embedded-components-and-admin-ui-server-interfaces"><span>Embedded <wbr/>Components and <wbr/>Admin <wbr/>UI / <wbr/>Server interfaces</span></a><ul><li><a href="#webpack-var-library"><span>Webpack (var library)</span></a></li><li><a href="#vite-esm-bundlers"><span>Vite / <wbr/>ESM bundlers</span></a></li><li><a href="#react-version-compatibility"><span>React <wbr/>Version <wbr/>Compatibility</span></a></li></ul><a href="#webapp-component-and-admin-ui--server-interfaces"><span>Web<wbr/>App / <wbr/>Component and <wbr/>Admin <wbr/>UI / <wbr/>Server interfaces</span></a><a href="#authentication-and-session-management"><span>Authentication and <wbr/>Session <wbr/>Management</span></a><ul><li><a href="#redirecting-unauthenticated-users-to-login"><span>Redirecting unauthenticated users to login</span></a></li></ul></div></details></div><div class="site-menu"><nav class="tsd-navigation"><a href="../../modules.html">Signal K</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>