@signalk/charts-plugin 3.5.0 → 3.6.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/package.json +5 -1
- package/plugin/chartDownloader.d.ts +1 -1
- package/plugin/chartDownloader.d.ts.map +1 -1
- package/plugin/chartDownloader.js +29 -19
- package/plugin/chartDownloader.js.map +1 -1
- package/plugin/charts.d.ts.map +1 -1
- package/plugin/charts.js +54 -31
- package/plugin/charts.js.map +1 -1
- package/plugin/index.d.ts.map +1 -1
- package/plugin/index.js +65 -101
- package/plugin/index.js.map +1 -1
- package/plugin/pluginStatus.d.ts +6 -0
- package/plugin/pluginStatus.d.ts.map +1 -0
- package/plugin/pluginStatus.js +36 -0
- package/plugin/pluginStatus.js.map +1 -0
- package/plugin/projection.d.ts.map +1 -1
- package/plugin/projection.js +14 -0
- package/plugin/projection.js.map +1 -1
- package/plugin/tileServer.d.ts +29 -0
- package/plugin/tileServer.d.ts.map +1 -0
- package/plugin/tileServer.js +162 -0
- package/plugin/tileServer.js.map +1 -0
- package/plugin/types.d.ts +21 -1
- package/plugin/types.d.ts.map +1 -1
- package/public/index.js +279 -240
- package/public/logo.svg +74 -0
package/public/index.js
CHANGED
|
@@ -1,193 +1,231 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
1
|
+
// POSTs a seed-job request and returns the parsed job info. Extracted so the
|
|
2
|
+
// fetch + response-validation logic can be unit-tested in node without jsdom.
|
|
3
|
+
// The browser still calls this from the click handler below.
|
|
4
|
+
async function submitSeedJob(chart, body, fetchImpl) {
|
|
5
|
+
const fetchFn = fetchImpl || globalThis.fetch
|
|
6
|
+
const resp = await fetchFn(
|
|
7
|
+
`/signalk/chart-tiles/cache/${encodeURIComponent(chart)}`,
|
|
8
|
+
{
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify(body)
|
|
12
|
+
}
|
|
7
13
|
)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if (!resp.ok) {
|
|
15
|
+
let text = ''
|
|
16
|
+
try {
|
|
17
|
+
text = await resp.text()
|
|
18
|
+
} catch (_readErr) {
|
|
19
|
+
// Ignore read errors when composing the error message; the status code
|
|
20
|
+
// and our surrounding context are enough to diagnose.
|
|
21
|
+
}
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Failed to seed charts: ${resp.status}${text ? ` - ${text}` : ''}`
|
|
24
|
+
)
|
|
16
25
|
}
|
|
26
|
+
const data = await resp.json()
|
|
27
|
+
if (!data || typeof data.id !== 'number') {
|
|
28
|
+
throw new Error('Job response is missing a numeric id')
|
|
29
|
+
}
|
|
30
|
+
return data
|
|
31
|
+
}
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
updateInputs() // initial load
|
|
33
|
+
// Node-only export for the unit test. The browser script tag ignores this
|
|
34
|
+
// branch because `module` is undefined in a plain <script>.
|
|
35
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
36
|
+
module.exports = { submitSeedJob }
|
|
37
|
+
}
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
// The browser attaches the DOM handlers; node (test context) skips them.
|
|
40
|
+
if (typeof document !== 'undefined') registerDomHandlers()
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
function registerDomHandlers() {
|
|
43
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
44
|
+
const regionSelect = document.getElementById('region')
|
|
45
|
+
const bboxInputs = document.querySelectorAll(
|
|
46
|
+
'#bboxMinLon, #bboxMinLat, #bboxMaxLon, #bboxMaxLat'
|
|
47
|
+
)
|
|
48
|
+
const tileInputs = document.querySelectorAll('#tileX, #tileY, #tileZ')
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const nb = b && b.name ? String(b.name) : ''
|
|
39
|
-
return na.localeCompare(nb)
|
|
40
|
-
})
|
|
41
|
-
.forEach(([id, info]) => {
|
|
42
|
-
const opt = document.createElement('option')
|
|
43
|
-
opt.value = id
|
|
44
|
-
opt.textContent = info && info.name ? info.name : id
|
|
45
|
-
regionSelect.appendChild(opt)
|
|
46
|
-
})
|
|
50
|
+
function updateInputs() {
|
|
51
|
+
const type = document.querySelector(
|
|
52
|
+
'input[name="inputType"]:checked'
|
|
53
|
+
).value
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
opt.value = ''
|
|
52
|
-
opt.textContent = '-- No regions available --'
|
|
53
|
-
regionSelect.appendChild(opt)
|
|
55
|
+
regionSelect.disabled = type !== 'region'
|
|
56
|
+
bboxInputs.forEach((i) => (i.disabled = type !== 'bbox'))
|
|
57
|
+
tileInputs.forEach((i) => (i.disabled = type !== 'tile'))
|
|
54
58
|
}
|
|
55
|
-
} catch (err) {
|
|
56
|
-
console.error('Error loading regions:', err)
|
|
57
|
-
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Attach listeners
|
|
61
|
+
document.querySelectorAll('input[name="inputType"]').forEach((r) => {
|
|
62
|
+
r.addEventListener('change', updateInputs)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
updateInputs() // initial load
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
const resp = await fetch('/signalk/v2/api/resources/charts')
|
|
65
|
-
if (!resp.ok) throw new Error(`Failed to fetch charts: ${resp.status}`)
|
|
66
|
-
const data = await resp.json()
|
|
67
|
-
// clear existing options
|
|
68
|
-
chartSelect.innerHTML = ''
|
|
67
|
+
if (!regionSelect) return
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
69
|
+
try {
|
|
70
|
+
const resp = await fetch('/signalk/v2/api/resources/regions')
|
|
71
|
+
if (!resp.ok) throw new Error(`Failed to fetch regions: ${resp.status}`)
|
|
72
|
+
const data = await resp.json()
|
|
73
|
+
// clear existing options
|
|
74
|
+
regionSelect.innerHTML = ''
|
|
75
|
+
|
|
76
|
+
const entries = Object.entries(data || {})
|
|
77
|
+
entries
|
|
78
|
+
.sort(([, a], [, b]) => {
|
|
79
|
+
const na = a && a.name ? String(a.name) : ''
|
|
80
|
+
const nb = b && b.name ? String(b.name) : ''
|
|
81
|
+
return na.localeCompare(nb)
|
|
82
|
+
})
|
|
83
|
+
.forEach(([id, info]) => {
|
|
79
84
|
const opt = document.createElement('option')
|
|
80
85
|
opt.value = id
|
|
81
86
|
opt.textContent = info && info.name ? info.name : id
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
})
|
|
87
|
+
regionSelect.appendChild(opt)
|
|
88
|
+
})
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// if no regions returned, add a fallback option
|
|
91
|
+
if (!regionSelect.options.length) {
|
|
92
|
+
const opt = document.createElement('option')
|
|
93
|
+
opt.value = ''
|
|
94
|
+
opt.textContent = '-- No regions available --'
|
|
95
|
+
regionSelect.appendChild(opt)
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('Error loading regions:', err)
|
|
92
99
|
}
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error('Error loading maps:', err)
|
|
95
|
-
}
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
.getElementById('
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const chart = document.getElementById('chart').value
|
|
102
|
-
const regionGUID = document.getElementById('region').value
|
|
103
|
-
const maxZoom = document.getElementById('maxZoom').value
|
|
104
|
-
const bbox = {
|
|
105
|
-
minLon: parseFloat(document.getElementById('bboxMinLon').value),
|
|
106
|
-
minLat: parseFloat(document.getElementById('bboxMinLat').value),
|
|
107
|
-
maxLon: parseFloat(document.getElementById('bboxMaxLon').value),
|
|
108
|
-
maxLat: parseFloat(document.getElementById('bboxMaxLat').value)
|
|
109
|
-
}
|
|
110
|
-
// const tile = {
|
|
111
|
-
// z: parseInt(document.getElementById('tileZ').value),
|
|
112
|
-
// x: parseInt(document.getElementById('tileX').value),
|
|
113
|
-
// y: parseInt(document.getElementById('tileY').value),
|
|
101
|
+
//Fetch available maps and populate the chart select box
|
|
102
|
+
const chartSelect = document.getElementById('chart')
|
|
103
|
+
if (!chartSelect) return
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
105
|
+
try {
|
|
106
|
+
const resp = await fetch('/signalk/v2/api/resources/charts')
|
|
107
|
+
if (!resp.ok) throw new Error(`Failed to fetch charts: ${resp.status}`)
|
|
108
|
+
const data = await resp.json()
|
|
109
|
+
// clear existing options
|
|
110
|
+
chartSelect.innerHTML = ''
|
|
111
|
+
|
|
112
|
+
const entries = Object.entries(data || {})
|
|
113
|
+
entries
|
|
114
|
+
.sort(([, a], [, b]) => {
|
|
115
|
+
const na = a && a.name ? String(a.name) : ''
|
|
116
|
+
const nb = b && b.name ? String(b.name) : ''
|
|
117
|
+
return na.localeCompare(nb)
|
|
118
|
+
})
|
|
119
|
+
.forEach(([id, info]) => {
|
|
120
|
+
if (info.proxy === true) {
|
|
121
|
+
const opt = document.createElement('option')
|
|
122
|
+
opt.value = id
|
|
123
|
+
opt.textContent = info && info.name ? info.name : id
|
|
124
|
+
chartSelect.appendChild(opt)
|
|
125
|
+
}
|
|
136
126
|
})
|
|
137
|
-
if (!resp.ok) throw new Error(`Failed to seed charts: ${resp.status}`)
|
|
138
|
-
const data = await resp.json()
|
|
139
|
-
} catch (err) {
|
|
140
|
-
console.error('Error seeding charts:', err)
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
127
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
128
|
+
// if no maps returned, add a fallback option
|
|
129
|
+
if (!chartSelect.options.length) {
|
|
130
|
+
const opt = document.createElement('option')
|
|
131
|
+
opt.value = ''
|
|
132
|
+
opt.textContent = '-- No maps available --'
|
|
133
|
+
chartSelect.appendChild(opt)
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('Error loading maps:', err)
|
|
153
137
|
}
|
|
154
|
-
}
|
|
155
138
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
139
|
+
document
|
|
140
|
+
.getElementById('createJobButton')
|
|
141
|
+
.addEventListener('click', async () => {
|
|
142
|
+
try {
|
|
143
|
+
const chart = document.getElementById('chart').value
|
|
144
|
+
const regionGUID = document.getElementById('region').value
|
|
145
|
+
const maxZoom = document.getElementById('maxZoom').value
|
|
146
|
+
const bbox = {
|
|
147
|
+
minLon: parseFloat(document.getElementById('bboxMinLon').value),
|
|
148
|
+
minLat: parseFloat(document.getElementById('bboxMinLat').value),
|
|
149
|
+
maxLon: parseFloat(document.getElementById('bboxMaxLon').value),
|
|
150
|
+
maxLat: parseFloat(document.getElementById('bboxMaxLat').value)
|
|
151
|
+
}
|
|
152
|
+
// const tile = {
|
|
153
|
+
// z: parseInt(document.getElementById('tileZ').value),
|
|
154
|
+
// x: parseInt(document.getElementById('tileX').value),
|
|
155
|
+
// y: parseInt(document.getElementById('tileY').value),
|
|
156
|
+
|
|
157
|
+
// };
|
|
158
|
+
// console.log(tile);
|
|
159
|
+
const type = document.querySelector(
|
|
160
|
+
'input[name="inputType"]:checked'
|
|
161
|
+
).value
|
|
162
|
+
const body = {}
|
|
163
|
+
if (type === 'region') {
|
|
164
|
+
body.regionGUID = regionGUID
|
|
165
|
+
} else if (type === 'bbox') {
|
|
166
|
+
body.bbox = bbox
|
|
167
|
+
}
|
|
168
|
+
// else if (type === 'tile') {
|
|
169
|
+
// body.tile = tile;
|
|
170
|
+
// }
|
|
171
|
+
body.maxZoom = maxZoom
|
|
172
|
+
await submitSeedJob(chart, body)
|
|
173
|
+
// Trigger an immediate refresh so the new job appears without waiting
|
|
174
|
+
// for the 2s polling interval.
|
|
175
|
+
fetchActiveJobs()
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('Error seeding charts:', err)
|
|
178
|
+
alert(`Failed to start seeding job: ${err.message}`)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const fmt = (n) => (typeof n === 'number' ? n.toLocaleString() : '-')
|
|
183
|
+
const statusText = (s) => {
|
|
184
|
+
switch (s) {
|
|
185
|
+
case 0:
|
|
186
|
+
return 'Stopped'
|
|
187
|
+
case 1:
|
|
188
|
+
return 'Running'
|
|
189
|
+
default:
|
|
190
|
+
return String(s)
|
|
191
|
+
}
|
|
165
192
|
}
|
|
166
193
|
|
|
167
|
-
items
|
|
168
|
-
const
|
|
194
|
+
function renderJobs(items) {
|
|
195
|
+
const tbody = document.querySelector('#activeJobsTable tbody')
|
|
196
|
+
if (!tbody) return
|
|
197
|
+
tbody.innerHTML = ''
|
|
198
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
199
|
+
const tr = document.createElement('tr')
|
|
200
|
+
tr.innerHTML = '<td colspan="10" class="muted">No active jobs</td>'
|
|
201
|
+
tbody.appendChild(tr)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
items.forEach((it, idx) => {
|
|
206
|
+
const tr = document.createElement('tr')
|
|
169
207
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
const pct =
|
|
209
|
+
typeof it.progress === 'number' && isFinite(it.progress)
|
|
210
|
+
? Math.max(0, Math.min(100, it.progress * 100))
|
|
211
|
+
: 0
|
|
212
|
+
const pctText = `${pct.toFixed(1)}%`
|
|
213
|
+
tr.innerHTML = [
|
|
214
|
+
`<td>${idx + 1}</td>`,
|
|
215
|
+
`<td>${it.chartName || '-'}</td>`,
|
|
216
|
+
`<td>${it.regionName || '-'}</td>`,
|
|
217
|
+
`<td>${fmt(it.totalTiles)}</td>`,
|
|
218
|
+
`<td>${fmt(it.downloadedTiles)}</td>`,
|
|
219
|
+
`<td>${fmt(it.cachedTiles)}</td>`,
|
|
220
|
+
`<td>${fmt(it.failedTiles)}</td>`,
|
|
221
|
+
`<td>
|
|
184
222
|
<span class="progress-bar" aria-hidden="true">
|
|
185
223
|
<span class="progress-fill" style="width:${pct}%"></span>
|
|
186
224
|
</span>
|
|
187
225
|
<span class="percent">${pctText}</span>
|
|
188
226
|
</td>`,
|
|
189
|
-
|
|
190
|
-
|
|
227
|
+
`<td>${statusText(it.status)}</td>`,
|
|
228
|
+
`
|
|
191
229
|
<td class="action-buttons">
|
|
192
230
|
<button class="btn startstop-btn" data-id="${it.id}" data-totalTiles="${it.totalTiles}" title="Start">
|
|
193
231
|
<svg class="icon-play ${it.status === 1 ? 'hidden' : ''}" viewBox="0 0 24 24" width="16" height="16" fill="white">
|
|
@@ -209,102 +247,103 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
209
247
|
</button>
|
|
210
248
|
</td>
|
|
211
249
|
`
|
|
212
|
-
|
|
250
|
+
].join('')
|
|
213
251
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
252
|
+
tr.querySelectorAll('.startstop-btn').forEach((button) => {
|
|
253
|
+
button.addEventListener('click', () => {
|
|
254
|
+
const playIcon = button.querySelector('.icon-play')
|
|
255
|
+
const pauseIcon = button.querySelector('.icon-pause')
|
|
256
|
+
const jobId = button.getAttribute('data-id')
|
|
257
|
+
const totalTiles = button.getAttribute('data-totalTiles')
|
|
258
|
+
const isRunning = pauseIcon.classList.contains('hidden')
|
|
259
|
+
console.log(isRunning)
|
|
260
|
+
if (isRunning) {
|
|
261
|
+
// Switch to stop
|
|
262
|
+
if (
|
|
263
|
+
totalTiles > 100000 &&
|
|
264
|
+
!confirm(
|
|
265
|
+
`This job has more than ${totalTiles} tiles to download. Starting it may take a long time and put high load on the server. Are you sure you want to start it?`
|
|
266
|
+
)
|
|
267
|
+
) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
takeAction(jobId, 'start')
|
|
271
|
+
button.title = 'Stop'
|
|
272
|
+
} else {
|
|
273
|
+
// Switch to start
|
|
274
|
+
takeAction(jobId, 'stop')
|
|
275
|
+
button.title = 'Start'
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
tr.querySelectorAll('.delete-btn').forEach((button) => {
|
|
281
|
+
button.addEventListener('click', () => {
|
|
224
282
|
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
`This job has more than ${totalTiles} tiles to download. Starting it may take a long time and put high load on the server. Are you sure you want to start it?`
|
|
283
|
+
confirm(
|
|
284
|
+
'This will delete all cached tiles for this job. Are you sure?'
|
|
228
285
|
)
|
|
229
286
|
) {
|
|
230
|
-
|
|
287
|
+
const jobId = button.getAttribute('data-id')
|
|
288
|
+
if (jobId) {
|
|
289
|
+
takeAction(jobId, 'delete')
|
|
290
|
+
}
|
|
231
291
|
}
|
|
232
|
-
|
|
233
|
-
button.title = 'Stop'
|
|
234
|
-
} else {
|
|
235
|
-
// Switch to start
|
|
236
|
-
takeAction(jobId, 'stop')
|
|
237
|
-
button.title = 'Start'
|
|
238
|
-
}
|
|
292
|
+
})
|
|
239
293
|
})
|
|
240
|
-
})
|
|
241
294
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
confirm(
|
|
246
|
-
'This will delete all cached tiles for this job. Are you sure?'
|
|
247
|
-
)
|
|
248
|
-
) {
|
|
295
|
+
tr.querySelectorAll('.remove-btn').forEach((button) => {
|
|
296
|
+
button.addEventListener('click', () => {
|
|
249
297
|
const jobId = button.getAttribute('data-id')
|
|
250
298
|
if (jobId) {
|
|
251
|
-
takeAction(jobId, '
|
|
299
|
+
takeAction(jobId, 'remove')
|
|
252
300
|
}
|
|
253
|
-
}
|
|
301
|
+
})
|
|
254
302
|
})
|
|
255
|
-
})
|
|
256
303
|
|
|
257
|
-
|
|
258
|
-
button.addEventListener('click', () => {
|
|
259
|
-
const jobId = button.getAttribute('data-id')
|
|
260
|
-
if (jobId) {
|
|
261
|
-
takeAction(jobId, 'remove')
|
|
262
|
-
}
|
|
263
|
-
})
|
|
304
|
+
tbody.appendChild(tr)
|
|
264
305
|
})
|
|
265
306
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
body: JSON.stringify({
|
|
276
|
-
action: action
|
|
277
|
-
})
|
|
278
|
-
})
|
|
279
|
-
.then((response) => {
|
|
280
|
-
if (!response.ok) {
|
|
281
|
-
throw new Error(
|
|
282
|
-
`Failed to ${action} job ${jobId}: ${response.status}`
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
fetchActiveJobs()
|
|
286
|
-
})
|
|
287
|
-
.catch((error) => {
|
|
288
|
-
console.error(`Error ${action} job:`, error)
|
|
307
|
+
function takeAction(jobId, action) {
|
|
308
|
+
fetch(`/signalk/chart-tiles/cache/jobs/${jobId}`, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: {
|
|
311
|
+
'Content-Type': 'application/json'
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify({
|
|
314
|
+
action: action
|
|
315
|
+
})
|
|
289
316
|
})
|
|
317
|
+
.then((response) => {
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Failed to ${action} job ${jobId}: ${response.status}`
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
fetchActiveJobs()
|
|
324
|
+
})
|
|
325
|
+
.catch((error) => {
|
|
326
|
+
console.error(`Error ${action} job:`, error)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
290
329
|
}
|
|
291
|
-
}
|
|
292
330
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
async function fetchActiveJobs() {
|
|
332
|
+
const tbody = document.querySelector('#activeJobsTable tbody')
|
|
333
|
+
try {
|
|
334
|
+
const resp = await fetch('/signalk/chart-tiles/cache/jobs')
|
|
335
|
+
if (!resp.ok) throw new Error('HTTP ' + resp.status)
|
|
336
|
+
const data = await resp.json()
|
|
337
|
+
renderJobs(data)
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error('Error fetching active jobs:', err)
|
|
340
|
+
tbody.innerHTML =
|
|
341
|
+
'<tr><td colspan="10" class="muted">Error loading active jobs</td></tr>'
|
|
342
|
+
}
|
|
304
343
|
}
|
|
305
|
-
}
|
|
306
344
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
})
|
|
345
|
+
fetchActiveJobs()
|
|
346
|
+
// refresh every 2 seconds
|
|
347
|
+
setInterval(fetchActiveJobs, 2000)
|
|
348
|
+
})
|
|
349
|
+
}
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Charts">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#1b6ea8"/>
|
|
5
|
+
<stop offset="1" stop-color="#0b3a5e"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="land" x1="0" y1="0" x2="0" y2="1">
|
|
8
|
+
<stop offset="0" stop-color="#f5e6b8"/>
|
|
9
|
+
<stop offset="1" stop-color="#d9c079"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<clipPath id="card">
|
|
12
|
+
<rect x="0" y="0" width="128" height="128" rx="22"/>
|
|
13
|
+
</clipPath>
|
|
14
|
+
</defs>
|
|
15
|
+
|
|
16
|
+
<g clip-path="url(#card)">
|
|
17
|
+
<rect width="128" height="128" fill="url(#sea)"/>
|
|
18
|
+
|
|
19
|
+
<g stroke="#bee3f4" stroke-opacity="0.35" stroke-width="1" fill="none">
|
|
20
|
+
<path d="M0 24 H128 M0 48 H128 M0 72 H128 M0 96 H128"/>
|
|
21
|
+
<path d="M24 0 V128 M48 0 V128 M72 0 V128 M96 0 V128"/>
|
|
22
|
+
</g>
|
|
23
|
+
|
|
24
|
+
<g fill="#bee3f4" fill-opacity="0.55" font-family="system-ui, sans-serif" font-size="6">
|
|
25
|
+
<text x="3" y="22">48°</text>
|
|
26
|
+
<text x="3" y="46">47°</text>
|
|
27
|
+
<text x="3" y="70">46°</text>
|
|
28
|
+
<text x="3" y="94">45°</text>
|
|
29
|
+
</g>
|
|
30
|
+
|
|
31
|
+
<path d="M14 92
|
|
32
|
+
Q 22 78, 36 80
|
|
33
|
+
T 60 70
|
|
34
|
+
Q 72 66, 78 54
|
|
35
|
+
Q 84 42, 100 40
|
|
36
|
+
Q 116 40, 128 48
|
|
37
|
+
L 128 128
|
|
38
|
+
L 0 128
|
|
39
|
+
L 0 96
|
|
40
|
+
Q 6 92, 14 92 Z"
|
|
41
|
+
fill="url(#land)" stroke="#8a6d2a" stroke-width="1" stroke-linejoin="round"/>
|
|
42
|
+
|
|
43
|
+
<g fill="#8a6d2a" fill-opacity="0.5">
|
|
44
|
+
<circle cx="46" cy="86" r="1.2"/>
|
|
45
|
+
<circle cx="58" cy="80" r="1"/>
|
|
46
|
+
<circle cx="70" cy="68" r="1.2"/>
|
|
47
|
+
<circle cx="88" cy="52" r="1"/>
|
|
48
|
+
<circle cx="106" cy="48" r="1.4"/>
|
|
49
|
+
</g>
|
|
50
|
+
|
|
51
|
+
<circle cx="34" cy="40" r="3.5" fill="url(#land)" stroke="#8a6d2a" stroke-width="0.8"/>
|
|
52
|
+
<circle cx="84" cy="22" r="2.2" fill="url(#land)" stroke="#8a6d2a" stroke-width="0.8"/>
|
|
53
|
+
|
|
54
|
+
<path d="M20 30 Q 44 34, 58 48 T 96 70"
|
|
55
|
+
fill="none" stroke="#e63946" stroke-width="1.6" stroke-linecap="round"
|
|
56
|
+
stroke-dasharray="3 3"/>
|
|
57
|
+
<circle cx="20" cy="30" r="2.4" fill="#e63946"/>
|
|
58
|
+
<g transform="translate(96 70)" stroke="#e63946" stroke-width="1.8" stroke-linecap="round">
|
|
59
|
+
<line x1="-3" y1="-3" x2="3" y2="3"/>
|
|
60
|
+
<line x1="-3" y1="3" x2="3" y2="-3"/>
|
|
61
|
+
</g>
|
|
62
|
+
|
|
63
|
+
<g transform="translate(108 104)">
|
|
64
|
+
<circle r="10" fill="#0b3a5e" fill-opacity="0.7" stroke="#bee3f4" stroke-width="0.8"/>
|
|
65
|
+
<polygon points="0,-8 2,0 0,0" fill="#e63946"/>
|
|
66
|
+
<polygon points="0,-8 -2,0 0,0" fill="#b5232f"/>
|
|
67
|
+
<polygon points="0,8 2,0 0,0" fill="#e2e8f0"/>
|
|
68
|
+
<polygon points="0,8 -2,0 0,0" fill="#cbd5e1"/>
|
|
69
|
+
<circle r="1" fill="#e2e8f0"/>
|
|
70
|
+
<text x="0" y="-11" text-anchor="middle" font-family="system-ui, sans-serif"
|
|
71
|
+
font-size="5" font-weight="700" fill="#e63946">N</text>
|
|
72
|
+
</g>
|
|
73
|
+
</g>
|
|
74
|
+
</svg>
|