@signalk/app-dock 0.3.0-beta.2 → 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.
- package/.github/workflows/release_on_tag.yml +29 -28
- package/package.json +1 -1
- package/public/dock.js +155 -43
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
name: 'Release on tag'
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
tags:
|
|
5
|
-
- '*'
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: release-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: false
|
|
6
11
|
|
|
7
12
|
jobs:
|
|
8
13
|
release:
|
|
9
14
|
permissions:
|
|
10
15
|
contents: write
|
|
11
|
-
if: startsWith(github.ref, 'refs/tags/')
|
|
12
16
|
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 10
|
|
13
18
|
steps:
|
|
14
|
-
- name: Build Changelog
|
|
15
|
-
id: github_release
|
|
16
|
-
uses: mikepenz/release-changelog-builder-action@v1
|
|
17
|
-
env:
|
|
18
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
19
|
-
|
|
20
19
|
- name: Create Release
|
|
21
|
-
uses:
|
|
20
|
+
uses: softprops/action-gh-release@v3
|
|
22
21
|
with:
|
|
23
|
-
tag_name: ${{ github.
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
tag_name: ${{ github.ref_name }}
|
|
23
|
+
name: ${{ github.ref_name }}
|
|
24
|
+
generate_release_notes: true
|
|
25
|
+
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }}
|
|
26
26
|
env:
|
|
27
27
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
28
28
|
|
|
@@ -32,25 +32,26 @@ jobs:
|
|
|
32
32
|
id-token: write
|
|
33
33
|
contents: read
|
|
34
34
|
runs-on: ubuntu-latest
|
|
35
|
+
timeout-minutes: 10
|
|
35
36
|
steps:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
|
|
39
|
+
- uses: actions/setup-node@v6
|
|
40
|
+
with:
|
|
41
|
+
node-version: '24.x'
|
|
42
|
+
registry-url: 'https://registry.npmjs.org'
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
run: |
|
|
44
|
-
npm install
|
|
44
|
+
- run: npm ci
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
then
|
|
46
|
+
- name: Publish to npm
|
|
47
|
+
run: |
|
|
48
|
+
tag="${GITHUB_REF#refs/tags/}"
|
|
49
|
+
if [[ "$tag" == *alpha* ]]; then
|
|
50
|
+
npm publish --provenance --access public --tag alpha
|
|
51
|
+
elif [[ "$tag" == *beta* ]]; then
|
|
53
52
|
npm publish --provenance --access public --tag beta
|
|
53
|
+
elif [[ "$tag" == *rc* ]]; then
|
|
54
|
+
npm publish --provenance --access public --tag rc
|
|
54
55
|
else
|
|
55
56
|
npm publish --provenance --access public
|
|
56
|
-
|
|
57
|
+
fi
|
package/package.json
CHANGED
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
'
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
$
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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()
|