signalk-czone 0.3.0-beta.16 → 0.3.0-beta.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0-beta.18
4
+
5
+ - Adds in-page Signal K sign-in for browser and installed home-screen apps, without leaving CZone (#35, thanks @superstar76).
6
+ - Remembers login across app restarts, preserves the form during refreshes, and shows clear login errors with the Admin UI login retained as a fallback.
7
+
8
+ ## 0.3.0-beta.17
9
+
10
+ - Prompts for Signal K login when CZone circuit requests are unauthorized, returning to the webapp after sign-in (#34).
11
+ - Shows permission and circuit-loading errors with a retry option instead of waiting indefinitely for Signal K.
12
+
3
13
  ## 0.3.0-beta.16
4
14
 
5
15
  - Adds an opt-in Victron GX switch pane for CZone circuits on the GX, Remote Console and VRM (#33).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-czone",
3
- "version": "0.3.0-beta.16",
3
+ "version": "0.3.0-beta.18",
4
4
  "description": "BETA: Signal K CZone monitoring and control using dynamically parsed ZCF configuration and NMEA 2000",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -42,7 +42,7 @@
42
42
  "icon.png"
43
43
  ],
44
44
  "scripts": {
45
- "test": "node test/zcf.test.js && node test/zcf-fixtures.test.js && node test/circuit-state.test.js && node test/czone.test.js && node test/nmea2000.test.js && node test/control-x-plus.test.js && node test/signalk.test.js && node test/write.test.js && node test/circuit-status.test.js && node test/status-mapping-regression.test.js && node test/state.test.js && node test/config-network.test.js && node test/config-namespace.test.js && node test/monitoring.test.js && node test/confirm-off.test.js && node test/webapp-current.test.js && node test/bridge.test.js && node test/victron-switches.test.js && node test/victron-label-steady.test.js && node test/switch-pane-plugin.test.js && node test/trends.test.js && node test/inputs.test.js && node --require ./test/helpers/gx-low-space.cjs test/trends.test.js"
45
+ "test": "node test/zcf.test.js && node test/zcf-fixtures.test.js && node test/circuit-state.test.js && node test/czone.test.js && node test/nmea2000.test.js && node test/control-x-plus.test.js && node test/signalk.test.js && node test/write.test.js && node test/circuit-status.test.js && node test/status-mapping-regression.test.js && node test/state.test.js && node test/config-network.test.js && node test/config-namespace.test.js && node test/monitoring.test.js && node test/confirm-off.test.js && node test/webapp-current.test.js && node test/webapp-auth.test.js && node test/bridge.test.js && node test/victron-switches.test.js && node test/victron-label-steady.test.js && node test/switch-pane-plugin.test.js && node test/trends.test.js && node test/inputs.test.js && node --require ./test/helpers/gx-low-space.cjs test/trends.test.js"
46
46
  },
47
47
  "dependencies": {
48
48
  "signalk-czone-zcf": "0.1.0-beta.2"
package/public/index.html CHANGED
@@ -75,6 +75,12 @@
75
75
  .hero h1, .hero p { position:relative; z-index:1; }
76
76
  .hero h1 { margin:0; font-size:30px; line-height:1.15; letter-spacing:-.7px; }
77
77
  .hero p { margin:9px 0 0; color:#a9c2d9; font-size:14px; }
78
+ .login-form { display:grid; gap:8px; max-width:280px; margin:10px 0; }
79
+ .login-form input { padding:10px 12px; border-radius:10px; border:1px solid var(--line); background:rgba(255,255,255,.05); color:var(--text); font-size:16px; }
80
+ .login-form button { padding:10px 12px; border-radius:10px; border:0; background:linear-gradient(135deg,#168eea,#2164b8); color:#fff; font-weight:700; font-size:15px; cursor:pointer; }
81
+ .login-form button:disabled { opacity:.6; cursor:default; }
82
+ .login-form .login-error { color:#ff8a9a; font-size:13px; }
83
+ .login-form .login-error:empty { display:none; }
78
84
  .search { position:relative; }
79
85
  .search input { width:100%; border:1px solid var(--line); background:#0b1d30; color:white; border-radius:999px; padding:11px 15px 11px 38px; outline:none; }
80
86
  .search input:focus { border-color:rgba(56,169,255,.65); box-shadow:0 0 0 3px rgba(56,169,255,.10); }
@@ -353,7 +359,7 @@
353
359
  <div class="connection" id="connection"><i></i><span>Connecting to Signal K…</span></div>
354
360
  <section class="hero">
355
361
  <h1 id="heroTitle">Your Boat, Your Circuits</h1>
356
- <p>Monitor and control your boat's electrical systems.</p><div id="writeGate" style="display:none;margin-top:12px;padding:10px 12px;border:1px solid rgba(255,195,77,.35);border-radius:10px;background:rgba(255,195,77,.08);color:#ffd98a;font-size:12px;line-height:1.5"></div>
362
+ <p>Monitor and control your boat's electrical systems.</p><div id="loadError" role="alert" hidden style="position:relative;z-index:1;margin-top:12px;padding:12px;border:1px solid var(--amber);border-radius:10px;background:var(--panel);line-height:1.5"></div><div id="writeGate" style="display:none;margin-top:12px;padding:10px 12px;border:1px solid rgba(255,195,77,.35);border-radius:10px;background:rgba(255,195,77,.08);color:#ffd98a;font-size:12px;line-height:1.5"></div>
357
363
  </section>
358
364
  <section class="section trend-panel" id="trendPanel">
359
365
  <div class="section-head"><span>📈</span><div><h2 id="trendTitle"></h2><span class="subtle" id="trendSub"></span></div><button type="button" class="sort trend-close" onclick="closeTrend()" aria-label="Close trend">✕ Close</button></div>
@@ -700,10 +706,90 @@
700
706
  if (Array.isArray(data.modes)) modes = data.modes;
701
707
  if (data.activeMode) activeMode = data.activeMode;
702
708
  }
709
+ function showLoadError(message, signIn) {
710
+ const el = document.querySelector('#loadError');
711
+ // Keep a sign-in form that is already showing: someone may be typing in it.
712
+ if (signIn && !el.hidden && el.loginForm) return;
713
+ el.loginForm = null;
714
+ el.textContent = message + ' ';
715
+ if (signIn) el.appendChild(loginForm(el));
716
+ const link = document.createElement('a');
717
+ link.style.color = 'var(--cyan)';
718
+ if (signIn) {
719
+ const here = window.location.pathname + window.location.search + window.location.hash;
720
+ link.href = '/admin/#/login?redirect=' + encodeURIComponent(here);
721
+ link.textContent = 'Or sign in on the Signal K login page';
722
+ } else {
723
+ link.href = window.location.href;
724
+ link.textContent = 'Retry';
725
+ link.onclick = event => { event.preventDefault(); window.location.reload(); };
726
+ }
727
+ el.appendChild(link);
728
+ el.hidden = false;
729
+ }
730
+
731
+ // Sign in on the page itself. A home-screen app (iPad, Android) keeps its
732
+ // own login, separate from the browser's, and has no address bar; the link
733
+ // to the admin page stays as the alternative. "Remember me" keeps the login
734
+ // after the app is closed, for as long as Signal K's token lasts.
735
+ function loginForm(banner) {
736
+ const form = document.createElement('form');
737
+ form.className = 'login-form';
738
+ const field = (name, type, autocomplete, placeholder) => {
739
+ const input = document.createElement('input');
740
+ input.name = name; input.type = type; input.required = true;
741
+ input.autocomplete = autocomplete; input.placeholder = placeholder;
742
+ form.appendChild(input);
743
+ return input;
744
+ };
745
+ const user = field('username', 'text', 'username', 'Signal K user');
746
+ const pass = field('password', 'password', 'current-password', 'Password');
747
+ const button = document.createElement('button');
748
+ button.type = 'submit'; button.textContent = 'Log in';
749
+ form.appendChild(button);
750
+ const error = document.createElement('span');
751
+ error.className = 'login-error';
752
+ form.appendChild(error);
753
+ form.onsubmit = async event => {
754
+ event.preventDefault();
755
+ error.textContent = '';
756
+ button.disabled = true;
757
+ try {
758
+ const r = await fetch('/signalk/v1/auth/login', { method:'POST', credentials:'include', headers:{ 'Content-Type':'application/json' },
759
+ body: JSON.stringify({ username: user.value, password: pass.value, rememberMe: true }) });
760
+ if (r.ok) { window.location.reload(); return; }
761
+ error.textContent = r.status === 401 ? 'Wrong user name or password.' : 'Signal K refused the login (HTTP ' + r.status + ').';
762
+ } catch (_) {
763
+ error.textContent = 'Signal K could not be reached.';
764
+ }
765
+ button.disabled = false;
766
+ };
767
+ banner.loginForm = form;
768
+ return form;
769
+ }
770
+
771
+ async function showCircuitRequestError(response) {
772
+ let signIn = response.status === 401;
773
+ if (response.status === 403) {
774
+ try {
775
+ const r = await fetch('/skServer/loginStatus', { cache:'no-store', credentials:'include' });
776
+ if (r.ok) signIn = (await r.json()).status === 'notLoggedIn';
777
+ } catch (_) { /* Keep the permission error if login status cannot be checked. */ }
778
+ }
779
+ const message = signIn
780
+ ? 'Sign in to Signal K to load CZone circuits.'
781
+ : response.status === 403
782
+ ? 'Signal K denied access to CZone circuits. Your account may not have the required permissions.'
783
+ : 'Could not load CZone circuits (HTTP ' + response.status + '). Check the Signal K server and CZone plugin.';
784
+ showLoadError(message, signIn);
785
+ // The plotter hides the banner, and its login is the Navico embedder's token.
786
+ updateSocketIndicator(signIn ? (MFD ? 'Sign in required: set the token in the Navico embedder' : 'Sign in required') : 'Unable to load CZone circuits', false);
787
+ }
788
+
703
789
  async function refreshOnce(reason = 'manual') {
704
790
  try {
705
791
  const r = await fetch(`${API}/circuits`, { cache:'no-store', credentials:'include' });
706
- if (!r.ok) throw new Error('Signal K plugin unavailable (HTTP ' + r.status + ')');
792
+ if (!r.ok) { await showCircuitRequestError(r); return false; }
707
793
  const data = await r.json();
708
794
  const incoming = Array.isArray(data.circuits) ? data.circuits : [];
709
795
  console.debug('[CZone UI] REST refresh', reason, { circuits: incoming.length, modes: Array.isArray(data.modes) ? data.modes.length : 0, sendingEnabled: data.sendingEnabled === true, nmeaReady: data.nmeaReady === true });
@@ -719,12 +805,14 @@
719
805
  : socketEverOpen
720
806
  ? `<i></i><span>Live updates lost · reconnecting… (circuits as of ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })})</span>`
721
807
  : '<i></i><span>Circuits loaded · connecting live updates…</span>';
808
+ document.querySelector('#loadError').hidden = true;
722
809
  render();
723
810
  return true;
724
811
  } catch (e) {
725
812
  console.warn('[CZone UI] REST refresh failed', reason, e);
726
813
  document.querySelector('#connection').classList.remove('ok');
727
- document.querySelector('#connection').innerHTML = '<i></i><span>Waiting for Signal K…</span>';
814
+ showLoadError('Could not load CZone circuits. Check the Signal K connection and CZone plugin. ' + e.message, false);
815
+ updateSocketIndicator('Unable to load CZone circuits', false);
728
816
  return false;
729
817
  }
730
818
  }
@@ -1334,7 +1422,8 @@
1334
1422
  }
1335
1423
 
1336
1424
  refreshVesselName();
1337
- refreshOnce('initial-load').then(() => {
1425
+ refreshOnce('initial-load').then(loaded => {
1426
+ if (!loaded) return;
1338
1427
  if (circuits.length) {
1339
1428
  connectSocket();
1340
1429
  sendSubscriptions();