@signalk/app-dock 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/dock.js +155 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/app-dock",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "macOS-style configurable app dock for switching between SignalK webapps on touch screens",
5
5
  "repository": {
6
6
  "url": "https://github.com/SignalK/app-dock"
package/public/dock.js CHANGED
@@ -1,6 +1,34 @@
1
1
  ;(async function DockApp() {
2
2
  'use strict'
3
3
 
4
+ // Bail out if we're running inside another iframe. This prevents the
5
+ // dock-in-dock loop when the dock is opened via its own Settings tile /
6
+ // the admin UI's Webapps tab / any other embed path. Same-origin redirect
7
+ // the top window to the dock URL so the user ends up on a clean
8
+ // standalone dock instead of a nested instance. The outer dock grants
9
+ // allow-top-navigation-by-user-activation in its iframe sandbox, so this
10
+ // works as long as the user activated the nested load by tapping.
11
+ if (window.self !== window.top) {
12
+ const dockUrl = window.location.href
13
+ try {
14
+ window.top.location.replace(dockUrl)
15
+ } catch {
16
+ // Cross-origin parent or missing top-nav sandbox permission.
17
+ }
18
+ // If replace() was blocked, top-nav sandbox permission was missing AND
19
+ // the user hadn't tapped recently. Show a tappable escape hatch — the
20
+ // click is itself a user activation, so the link succeeds even when
21
+ // only allow-top-navigation-by-user-activation is present.
22
+ document.body.innerHTML =
23
+ '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;height:100vh;color:#fff;background:#000;font-family:-apple-system,BlinkMacSystemFont,sans-serif;text-align:center;padding:16px">' +
24
+ '<img src="app-icon.svg" alt="App Dock" style="width:72px;height:72px;border-radius:16px;opacity:0.85" />' +
25
+ '<a href="' +
26
+ dockUrl +
27
+ '" target="_top" style="color:#fff;text-decoration:underline;font-size:16px">Open App Dock</a>' +
28
+ '</div>'
29
+ return
30
+ }
31
+
4
32
  const DEFAULTS = {
5
33
  position: 'bottom',
6
34
  iframeMode: 'keep-alive',
@@ -12,26 +40,85 @@
12
40
  apps: []
13
41
  }
14
42
 
43
+ // Fallback app list used when we can't read server config — i.e. the
44
+ // visitor has no admin access (/plugins/* requires admin in signalk-server,
45
+ // so anonymous, readonly and readwrite users all 401/403). Mirrors the
46
+ // server-side DEFAULT_APPS in plugin/index.js so the demo at
47
+ // demo.signalk.org behaves sensibly for non-admin visitors.
48
+ const NO_ADMIN_DEFAULTS = {
49
+ showNightModeButton: true,
50
+ apps: [
51
+ {
52
+ enabled: true,
53
+ autostart: true,
54
+ url: '/@signalk/freeboard-sk/',
55
+ label: 'Freeboard-SK',
56
+ icon: '/@signalk/freeboard-sk/assets/icons/icon-72x72.png',
57
+ color: ''
58
+ },
59
+ {
60
+ enabled: true,
61
+ autostart: false,
62
+ url: '/@mxtommy/kip/',
63
+ label: 'KIP',
64
+ icon: '/@mxtommy/kip/assets/icon-72x72.png',
65
+ color: ''
66
+ },
67
+ {
68
+ enabled: true,
69
+ autostart: false,
70
+ url: '/admin/',
71
+ label: 'Settings',
72
+ icon: '/@signalk/app-dock/icon-settings.svg',
73
+ color: '#78788c'
74
+ }
75
+ ]
76
+ }
77
+
78
+ // ─── Detect admin access ────────────────────────────────────────────────────
79
+ // /skServer/loginStatus is public even when allow_readonly is on and
80
+ // /plugins/* is admin-gated. Only logged-in admins can read plugin
81
+ // settings — anonymous, readonly and readwrite users all 401/403. We set
82
+ // noAdminAccess accordingly and take the fallback path for all of them.
83
+ // Try /skServer/loginStatus first, fall back to the legacy /loginStatus.
84
+ let noAdminAccess = false
85
+ for (const url of ['/skServer/loginStatus', '/loginStatus']) {
86
+ try {
87
+ const res = await fetch(url)
88
+ if (res.ok) {
89
+ const data = await res.json()
90
+ noAdminAccess = !(data.status === 'loggedIn' && data.userLevel === 'admin')
91
+ break
92
+ }
93
+ } catch {
94
+ // try next URL
95
+ }
96
+ }
97
+
15
98
  // ─── Load config from plugin endpoints ───────────────────────────────────────
16
99
  let cfg = { ...DEFAULTS }
17
- try {
18
- const [configRes, settingsRes] = await Promise.all([
19
- fetch('/plugins/signalk-app-dock/config'),
20
- fetch('/plugins/signalk-app-dock/settings')
21
- ])
22
- if (configRes.ok) {
23
- const data = await configRes.json()
24
- const pluginCfg = data.configuration || data
25
- cfg = { ...DEFAULTS, ...pluginCfg }
26
- }
27
- if (settingsRes.ok) {
28
- const data = await settingsRes.json()
29
- if (Array.isArray(data.apps) && data.apps.length > 0) {
30
- cfg.apps = data.apps
100
+ if (noAdminAccess) {
101
+ cfg = { ...DEFAULTS, ...NO_ADMIN_DEFAULTS }
102
+ } else {
103
+ try {
104
+ const [configRes, settingsRes] = await Promise.all([
105
+ fetch('/plugins/signalk-app-dock/config'),
106
+ fetch('/plugins/signalk-app-dock/settings')
107
+ ])
108
+ if (configRes.ok) {
109
+ const data = await configRes.json()
110
+ const pluginCfg = data.configuration || data
111
+ cfg = { ...DEFAULTS, ...pluginCfg }
31
112
  }
113
+ if (settingsRes.ok) {
114
+ const data = await settingsRes.json()
115
+ if (Array.isArray(data.apps) && data.apps.length > 0) {
116
+ cfg.apps = data.apps
117
+ }
118
+ }
119
+ } catch (e) {
120
+ console.warn('[Dock] Could not load config, using defaults.', e)
32
121
  }
33
- } catch (e) {
34
- console.warn('[Dock] Could not load config, using defaults.', e)
35
122
  }
36
123
 
37
124
  if (!Array.isArray(cfg.apps) || cfg.apps.length === 0) {
@@ -56,6 +143,7 @@
56
143
  let currentMode = 'day'
57
144
 
58
145
  async function fetchCurrentMode() {
146
+ if (noAdminAccess) return
59
147
  try {
60
148
  const res = await fetch('/plugins/signalk-app-dock/mode')
61
149
  if (res.ok) {
@@ -77,6 +165,7 @@
77
165
  }
78
166
 
79
167
  async function toggleNightMode() {
168
+ if (noAdminAccess) return
80
169
  await fetchCurrentMode()
81
170
  const newMode = currentMode === 'night' ? 'day' : 'night'
82
171
  try {
@@ -121,16 +210,23 @@
121
210
  }
122
211
 
123
212
  if (!cfg.apps || cfg.apps.length === 0) {
124
- $idleHintText.innerHTML =
125
- 'No apps configured yet.<br>' +
126
- 'Open <strong>Admin UI \u2192 Plugin Config \u2192 App Dock</strong><br>' +
127
- 'and click <strong>Discover Installed Webapps</strong> to get started.'
213
+ if (noAdminAccess) {
214
+ $idleHintText.innerHTML =
215
+ 'App Dock can only be customised by an administrator.<br>' +
216
+ 'Ask an admin to set up apps in <strong>Plugin Config</strong>.'
217
+ } else {
218
+ $idleHintText.innerHTML =
219
+ 'No apps configured yet.<br>' +
220
+ 'Open <strong>Admin UI \u2192 Plugin Config \u2192 App Dock</strong><br>' +
221
+ 'and click <strong>Discover Installed Webapps</strong> to get started.'
222
+ }
128
223
  return
129
224
  }
130
225
 
131
- $idleHintText.innerHTML =
132
- 'Double-tap <strong>anywhere</strong> to open the dock' +
133
- '<br><br><span style="font-size:12px;opacity:0.6">Configure in Admin UI \u2192 Plugin Config \u2192 App Dock</span>'
226
+ const footer = noAdminAccess
227
+ ? '<br><br><span style="font-size:12px;opacity:0.6">Customisation is admin-only</span>'
228
+ : '<br><br><span style="font-size:12px;opacity:0.6">Configure in Admin UI \u2192 Plugin Config \u2192 App Dock</span>'
229
+ $idleHintText.innerHTML = 'Double-tap <strong>anywhere</strong> to open the dock' + footer
134
230
  }
135
231
 
136
232
  function hideIdleHint() {
@@ -466,7 +562,8 @@
466
562
 
467
563
  const $frame = document.createElement('iframe')
468
564
  $frame.allow = 'fullscreen; geolocation'
469
- $frame.sandbox = 'allow-same-origin allow-scripts allow-forms allow-popups allow-pointer-lock'
565
+ $frame.sandbox =
566
+ 'allow-same-origin allow-scripts allow-forms allow-popups allow-pointer-lock allow-top-navigation-by-user-activation'
470
567
  $frame.style.touchAction = 'auto'
471
568
 
472
569
  $frame.addEventListener('load', hideLoading, { once: true })
@@ -627,7 +724,7 @@
627
724
  // ─── Init ────────────────────────────────────────────────────────────────────
628
725
  buildDock()
629
726
 
630
- if (cfg.showNightModeButton) {
727
+ if (cfg.showNightModeButton && !noAdminAccess) {
631
728
  fetchCurrentMode()
632
729
  setInterval(fetchCurrentMode, 5000)
633
730
  }
@@ -679,7 +776,7 @@
679
776
  }
680
777
  }
681
778
 
682
- setInterval(refreshConfig, 5000)
779
+ if (!noAdminAccess) setInterval(refreshConfig, 5000)
683
780
 
684
781
  // ─── Welcome tour ────────────────────────────────────────────────────────────
685
782
  const $tourOverlay = document.getElementById('tour-overlay')
@@ -697,7 +794,15 @@
697
794
  }
698
795
 
699
796
  if ($tourGotIt) $tourGotIt.addEventListener('click', hideTour)
700
- if ($tourDismiss) {
797
+
798
+ // Anonymous visitors can't persist tour dismissal server-side (POST
799
+ // /dismiss-tour requires auth), so we drop the "Don't show again" button
800
+ // rather than advertise a setting we can't honour. The tour reappears on
801
+ // next visit — correct semantics for a read-only visitor.
802
+ if (noAdminAccess) {
803
+ if ($tourDismiss) $tourDismiss.remove()
804
+ document.getElementById('tour-buttons')?.style.setProperty('justify-content', 'center')
805
+ } else if ($tourDismiss) {
701
806
  $tourDismiss.addEventListener('click', async () => {
702
807
  hideTour()
703
808
  try {
@@ -710,22 +815,29 @@
710
815
 
711
816
  const $tourLink = document.getElementById('tour-link')
712
817
  if ($tourLink) {
713
- $tourLink.addEventListener('click', (e) => {
714
- e.preventDefault()
715
- const adminUrl = '/admin/#/serverConfiguration/plugins/signalk-app-dock'
716
- hideIdleHint()
717
- const $frame = document.createElement('iframe')
718
- $frame.allow = 'fullscreen; geolocation'
719
- $frame.sandbox = 'allow-same-origin allow-scripts allow-forms allow-popups allow-pointer-lock'
720
- $frame.style.touchAction = 'auto'
721
- $frame.classList.add('active')
722
- $iframeContainer.innerHTML = ''
723
- Object.keys(iframes).forEach((k) => delete iframes[k])
724
- $iframeContainer.appendChild($frame)
725
- $frame.src = adminUrl
726
- iframes[adminUrl] = $frame
727
- hideTour()
728
- })
818
+ if (noAdminAccess) {
819
+ // Non-admin visitors can't reach Plugin Config; replace the link with
820
+ // plain text so the tour doesn't advertise a dead-end.
821
+ $tourLink.replaceWith(document.createTextNode('Plugin Config (admin only)'))
822
+ } else {
823
+ $tourLink.addEventListener('click', (e) => {
824
+ e.preventDefault()
825
+ const adminUrl = '/admin/#/serverConfiguration/plugins/signalk-app-dock'
826
+ hideIdleHint()
827
+ const $frame = document.createElement('iframe')
828
+ $frame.allow = 'fullscreen; geolocation'
829
+ $frame.sandbox =
830
+ 'allow-same-origin allow-scripts allow-forms allow-popups allow-pointer-lock allow-top-navigation-by-user-activation'
831
+ $frame.style.touchAction = 'auto'
832
+ $frame.classList.add('active')
833
+ $iframeContainer.innerHTML = ''
834
+ Object.keys(iframes).forEach((k) => delete iframes[k])
835
+ $iframeContainer.appendChild($frame)
836
+ $frame.src = adminUrl
837
+ iframes[adminUrl] = $frame
838
+ hideTour()
839
+ })
840
+ }
729
841
  }
730
842
 
731
843
  if (!cfg.tourDismissed) showTour()