signalk-race-control 0.4.0 → 0.4.2
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/README.md +4 -4
- package/index.js +35 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,10 +49,10 @@ and a live projected finishing order — during and after the race.
|
|
|
49
49
|
that name is added to any race. A boat that *is* VET-matched never has its TCF
|
|
50
50
|
carried over this way — it always starts from the default until you pick a VET
|
|
51
51
|
alternative or edit it again.
|
|
52
|
-
- **MMSI, remembered across races** — set once per boat (
|
|
53
|
-
from a live AIS/self vessel with a matching name),
|
|
54
|
-
cross-race registry: add a boat with the same name
|
|
55
|
-
|
|
52
|
+
- **MMSI and sail number, remembered across races** — set once per boat (MMSI can
|
|
53
|
+
also be picked up automatically from a live AIS/self vessel with a matching name),
|
|
54
|
+
both are remembered in a small cross-race registry: add a boat with the same name
|
|
55
|
+
in a later race and its MMSI and sail number fill in on their own.
|
|
56
56
|
- **Import a whole fleet from Manage2Sail** — off by default (`raceImportEnabled` in
|
|
57
57
|
the plugin's settings, alongside `vetEnabled`); once turned on, an **Import boats
|
|
58
58
|
from Manage2Sail** section lets you paste a Manage2Sail event URL, pick one or more
|
package/index.js
CHANGED
|
@@ -377,13 +377,27 @@ function buildOfflineTimerHtml(race, defaultTcf, vetOptions) {
|
|
|
377
377
|
// prematurely close the tag, e.g. via "</script>" in a boat name) is
|
|
378
378
|
// escaped. JSON.parse doesn't need this reversed; < is valid JSON.
|
|
379
379
|
const seedJson = JSON.stringify(seed).replace(/</g, '\\u003c');
|
|
380
|
+
// The race name also goes straight into HTML attributes/text below (page
|
|
381
|
+
// title, the iOS home-screen app title) — escaped the normal way for that
|
|
382
|
+
// context, distinct from the JSON escaping above.
|
|
383
|
+
const escapedRaceName = String(race.name || 'Race').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
380
384
|
|
|
381
385
|
return `<!doctype html>
|
|
382
386
|
<html lang="en">
|
|
383
387
|
<head>
|
|
384
388
|
<meta charset="utf-8" />
|
|
385
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
386
|
-
<title
|
|
389
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
390
|
+
<title>${escapedRaceName} — Offline Timer</title>
|
|
391
|
+
<meta name="theme-color" content="#0f172a" />
|
|
392
|
+
<!-- Lets "Add to Home Screen" on iOS launch this as a standalone app rather
|
|
393
|
+
than a Safari bookmark — standalone home-screen apps get their own
|
|
394
|
+
persistent storage that isn't subject to Safari's usual after-a-week
|
|
395
|
+
cleanup of unvisited sites, which is what actually makes the saved race
|
|
396
|
+
state stick around between uses. -->
|
|
397
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
398
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
399
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
400
|
+
<meta name="apple-mobile-web-app-title" content="${escapedRaceName}" />
|
|
387
401
|
<style>
|
|
388
402
|
:root {
|
|
389
403
|
--bg: #0f172a; --panel: #1e293b; --border: #334155; --text: #e2e8f0;
|
|
@@ -392,7 +406,7 @@ function buildOfflineTimerHtml(race, defaultTcf, vetOptions) {
|
|
|
392
406
|
* { box-sizing: border-box; }
|
|
393
407
|
[hidden] { display: none !important; }
|
|
394
408
|
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); }
|
|
395
|
-
header { padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border); text-align: center; }
|
|
409
|
+
header { padding: max(1.25rem, env(safe-area-inset-top)) 1.5rem 1.25rem; border-bottom: 1px solid var(--border); text-align: center; }
|
|
396
410
|
h1 { margin: 0 0 0.4rem; font-size: 1.1rem; font-weight: 600; letter-spacing: 0.02em; color: var(--muted); text-transform: uppercase; }
|
|
397
411
|
h2 { margin: 0 0 0.75rem; font-size: 1.3rem; }
|
|
398
412
|
.offline-note { max-width: 40rem; margin: 0 auto 1rem; font-size: 0.8rem; color: var(--muted); }
|
|
@@ -411,7 +425,7 @@ button.confirming { background: var(--bad); color: #2a0a0a; border-color: var(--
|
|
|
411
425
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
412
426
|
.status { min-height: 1.2em; margin-top: 0.5rem; font-size: 0.85rem; color: var(--muted); }
|
|
413
427
|
.status.error { color: var(--bad); }
|
|
414
|
-
main { padding: 1rem 1.5rem 2rem; max-width: 900px; margin: 0 auto; }
|
|
428
|
+
main { padding: 1rem 1.5rem max(2rem, env(safe-area-inset-bottom)); max-width: 900px; margin: 0 auto; }
|
|
415
429
|
.add-boat-row { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
416
430
|
.add-boat-row input[type='text'] { flex: 1; min-width: 12rem; padding: 0.45rem 0.6rem; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; }
|
|
417
431
|
.add-boat-row input[type='number'] { width: 6rem; padding: 0.45rem 0.6rem; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; }
|
|
@@ -1321,9 +1335,10 @@ module.exports = function (app) {
|
|
|
1321
1335
|
|
|
1322
1336
|
// Races are kept by id so several can be planned ahead and reviewed after
|
|
1323
1337
|
// the fact, rather than a single race getting overwritten by Reset.
|
|
1324
|
-
// boatRegistry is a separate, cross-race name->MMSI/TCF
|
|
1325
|
-
// boat's MMSI or (for boats outside VET) TCF
|
|
1326
|
-
// applied automatically next time that name is
|
|
1338
|
+
// boatRegistry is a separate, cross-race name->MMSI/sail number/TCF
|
|
1339
|
+
// memory: once a boat's MMSI, sail number, or (for boats outside VET) TCF
|
|
1340
|
+
// is entered anywhere, it's applied automatically next time that name is
|
|
1341
|
+
// used in any race.
|
|
1327
1342
|
let state = { races: {}, order: [], currentRaceId: null, boatRegistry: {} };
|
|
1328
1343
|
let dataFile = null;
|
|
1329
1344
|
const scheduleTimers = new Map(); // raceId -> Timeout, for scheduledStart
|
|
@@ -1346,14 +1361,16 @@ module.exports = function (app) {
|
|
|
1346
1361
|
}
|
|
1347
1362
|
|
|
1348
1363
|
// Cross-race memory keyed by boat name, merging in whichever fields are
|
|
1349
|
-
// given (mmsi and/or tcf) without clobbering the
|
|
1350
|
-
// written here for boats outside the VET register — see the
|
|
1364
|
+
// given (mmsi, sailNumber, and/or tcf) without clobbering the others. tcf
|
|
1365
|
+
// is only ever written here for boats outside the VET register — see the
|
|
1366
|
+
// callers.
|
|
1351
1367
|
function upsertBoatRegistry(name, fields) {
|
|
1352
1368
|
if (!name) return;
|
|
1353
1369
|
const key = name.trim().toLowerCase();
|
|
1354
1370
|
const existing = state.boatRegistry[key] || { name: name.trim() };
|
|
1355
1371
|
existing.name = name.trim();
|
|
1356
1372
|
if (fields.mmsi) existing.mmsi = String(fields.mmsi).trim();
|
|
1373
|
+
if (fields.sailNumber) existing.sailNumber = String(fields.sailNumber).trim();
|
|
1357
1374
|
if (fields.tcf != null) existing.tcf = fields.tcf;
|
|
1358
1375
|
state.boatRegistry[key] = existing;
|
|
1359
1376
|
}
|
|
@@ -1362,11 +1379,6 @@ module.exports = function (app) {
|
|
|
1362
1379
|
return state.boatRegistry[(name || '').trim().toLowerCase()] || null;
|
|
1363
1380
|
}
|
|
1364
1381
|
|
|
1365
|
-
function lookupBoatRegistry(name) {
|
|
1366
|
-
const entry = getRegistryEntry(name);
|
|
1367
|
-
return entry ? entry.mmsi : null;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
1382
|
// vetEnabled is a plugin config setting (Server -> Plugin Config), not
|
|
1371
1383
|
// per-race and not editable from the webapp itself. Off by default — a
|
|
1372
1384
|
// fresh install (or one that's never touched this setting) treats every
|
|
@@ -2151,23 +2163,26 @@ module.exports = function (app) {
|
|
|
2151
2163
|
if (!name) {
|
|
2152
2164
|
return res.status(400).json({ error: 'name is required' });
|
|
2153
2165
|
}
|
|
2166
|
+
const registryEntry = getRegistryEntry(name);
|
|
2154
2167
|
const givenMmsi = ((req.body && req.body.mmsi) || '').toString().trim();
|
|
2155
|
-
const mmsi = givenMmsi ||
|
|
2168
|
+
const mmsi = givenMmsi || (registryEntry && registryEntry.mmsi) || null;
|
|
2156
2169
|
if (mmsi) upsertBoatRegistry(name, { mmsi });
|
|
2170
|
+
const givenSailNumber = ((req.body && req.body.sailNumber) || '').toString().trim();
|
|
2171
|
+
const sailNumber = givenSailNumber || (registryEntry && registryEntry.sailNumber) || null;
|
|
2172
|
+
if (sailNumber) upsertBoatRegistry(name, { sailNumber });
|
|
2157
2173
|
const defaultTcf = (plugin.options && plugin.options.defaultTcf) || 1.0;
|
|
2158
2174
|
// A remembered TCF only applies to boats outside VET — a VET-matched
|
|
2159
2175
|
// boat should be picked fresh from the register's own dropdown rather
|
|
2160
2176
|
// than silently carrying over a number from wherever it last raced.
|
|
2161
2177
|
let tcf = defaultTcf;
|
|
2162
|
-
if (!isVetMatch(name)) {
|
|
2163
|
-
|
|
2164
|
-
if (remembered && remembered.tcf != null) tcf = remembered.tcf;
|
|
2178
|
+
if (!isVetMatch(name) && registryEntry && registryEntry.tcf != null) {
|
|
2179
|
+
tcf = registryEntry.tcf;
|
|
2165
2180
|
}
|
|
2166
2181
|
const boat = {
|
|
2167
2182
|
id: makeBoatId(),
|
|
2168
2183
|
name,
|
|
2169
2184
|
mmsi: mmsi || null,
|
|
2170
|
-
sailNumber
|
|
2185
|
+
sailNumber,
|
|
2171
2186
|
tcf,
|
|
2172
2187
|
finishTime: null,
|
|
2173
2188
|
startTime: null,
|
|
@@ -2278,6 +2293,7 @@ module.exports = function (app) {
|
|
|
2278
2293
|
if (!boat) return res.status(404).json({ error: 'No such boat' });
|
|
2279
2294
|
const sailNumber = ((req.body && req.body.sailNumber) || '').toString().trim();
|
|
2280
2295
|
boat.sailNumber = sailNumber || null;
|
|
2296
|
+
if (boat.sailNumber) upsertBoatRegistry(boat.name, { sailNumber: boat.sailNumber });
|
|
2281
2297
|
saveState();
|
|
2282
2298
|
res.json(boat);
|
|
2283
2299
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-race-control",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "SignalK plugin and webapp for tracking elapsed and handicap-corrected (Time-on-Time) race time, with a per-boat editable TCF and boat names pulled from AIS when available.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|