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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/index.js +35 -19
  3. 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 (or picked up automatically
53
- from a live AIS/self vessel with a matching name), it's remembered in a small
54
- cross-race registry: add a boat with the same name in a later race and its MMSI
55
- fills in on its own.
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>Race Control — Offline Timer</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 memory: once a
1325
- // boat's MMSI or (for boats outside VET) TCF is entered anywhere, it's
1326
- // applied automatically next time that name is used in any race.
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 other. tcf is only ever
1350
- // written here for boats outside the VET register — see the callers.
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 || lookupBoatRegistry(name);
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
- const remembered = getRegistryEntry(name);
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: null,
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.0",
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": [