diff --git a/dashboard/app.js b/dashboard/app.js index 2857cc3..e660148 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -15,7 +15,9 @@ class DashboardApp { responseTemplate: document.getElementById('responseTemplate'), incomingMessage: document.getElementById('incomingMessage'), chargerTableBody: document.getElementById('chargerTableBody'), - chargerEmptyRow: document.getElementById('chargerEmptyRow') + chargerEmptyRow: document.getElementById('chargerEmptyRow'), + panelButtons: Array.from(document.querySelectorAll('[data-panel-target]')), + contentPanels: Array.from(document.querySelectorAll('[data-panel]')) }; this.sessionKey = 'evdash.session'; @@ -28,6 +30,7 @@ class DashboardApp { this.tokenRefreshTimer = null; this.refreshInFlight = false; this.chargers = new Map(); + this.activePanel = null; this.chargerColumns = [ { key: 'id', label: 'ID', hidden: true }, { key: 'name', label: 'Name' }, @@ -44,6 +47,7 @@ class DashboardApp { this.renderStaticTemplates(); this.attachEventListeners(); + this.initializePanelNavigation(); this.restoreSession(); this.toggleChargerEmptyState(); } @@ -63,6 +67,83 @@ class DashboardApp { } } + initializePanelNavigation() { + const buttons = Array.isArray(this.elements.panelButtons) ? this.elements.panelButtons : []; + const panels = Array.isArray(this.elements.contentPanels) ? this.elements.contentPanels : []; + if (!buttons.length || !panels.length) + return; + + const activatePanel = target => { + if (!target) + return; + + const hasTarget = panels.some(panel => panel.dataset.panel === target); + if (!hasTarget) + return; + + this.activePanel = target; + panels.forEach(panel => { + const isActive = panel.dataset.panel === target; + panel.classList.toggle('active', isActive); + panel.setAttribute('aria-hidden', isActive ? 'false' : 'true'); + }); + + buttons.forEach(button => { + const isActive = button.dataset.panelTarget === target; + button.classList.toggle('active', isActive); + button.setAttribute('aria-pressed', isActive ? 'true' : 'false'); + }); + + const desiredHash = `#${target}`; + if (window.location.hash !== desiredHash) { + try { + window.history.replaceState(null, '', desiredHash); + } catch (error) { + window.location.hash = target; + } + } + }; + + buttons.forEach(button => { + button.addEventListener('click', () => { + activatePanel(button.dataset.panelTarget); + }); + }); + + const hashPanel = this.normalizePanelTargetFromHash(window.location.hash); + const preselected = buttons.find(button => button.classList.contains('active')); + const fallback = buttons[0]; + const initialTarget = hashPanel + || (preselected ? preselected.dataset.panelTarget : null) + || (fallback ? fallback.dataset.panelTarget : null); + + if (initialTarget) + activatePanel(initialTarget); + + window.addEventListener('hashchange', () => { + const target = this.normalizePanelTargetFromHash(window.location.hash); + if (target && target !== this.activePanel) + activatePanel(target); + }); + } + + normalizePanelTargetFromHash(hash) { + if (!hash || hash.length < 2) + return null; + + const lookup = hash.replace('#', '').trim().toLowerCase(); + if (!lookup) + return null; + + const panels = Array.isArray(this.elements.contentPanels) ? this.elements.contentPanels : []; + const match = panels.find(panel => { + const id = panel.dataset.panel || ''; + return id.toLowerCase() === lookup; + }); + + return match ? match.dataset.panel : null; + } + renderStaticTemplates() { const contract = { login: { @@ -767,7 +848,7 @@ class DashboardApp { setAuthLayout(requireAuth) { const body = document.body; - if (!body || body.dataset.mode === 'help') + if (!body) return; body.classList.toggle('needs-auth', requireAuth); } diff --git a/dashboard/help.html b/dashboard/help.html index 1a2cfee..d0b29f6 100644 --- a/dashboard/help.html +++ b/dashboard/help.html @@ -2,20 +2,17 @@ + EV Dash · Help - -
-
-
- -
-

EV Dash

-

Reference & diagnostics

-
-
-
-

- - Awaiting login… -

-
- - Load the dashboard to authenticate. -
- - -
-
-
- -
-
-

API Contract

-

All requests follow the structure below. Use app.sendAction(action, payload) from the browser console after authenticating on the dashboard.

-
-
-

Request template

-

-                
-
-

Responses

-

-                
-
-
- -
-

Last WebSocket message

-

Inspect the raw payload received from the nymea backend. Sign in on the dashboard first so this page can reuse the stored session.

-
No messages received yet.
-
- -
-

Charger table basics

- -
+ +
+ EV Dash logo +

Help moved into the dashboard

+

The help content now lives inside the built-in side panel. You will be redirected automatically.

+ Open EV Dash
- - - - + diff --git a/dashboard/index.html b/dashboard/index.html index b835096..7b3fca4 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -8,12 +8,12 @@ @@ -408,7 +530,7 @@

EV Dash

-

Monitor EV chargers in real time.

+

Monitor & troubleshoot EV chargers.

@@ -421,37 +543,116 @@ Awaiting login…
- ? +
-
-

Chargers

-
- - - - - - - - - - - - - - - - - - - - -
NameConnectedCharging currentCharging allowedCurrent powerPlugged inVersionSession energyTemperatureCharging phases
No chargers loaded yet.
-
-
+
+ + +
+
+
+
+
+

Live overview

+

Chargers

+
+
+
+ + + + + + + + + + + + + + + + + + + + +
NameConnectedCharging currentCharging allowedCurrent powerPlugged inVersionSession energyTemperatureCharging phases
No chargers loaded yet.
+
+
+
+ + +
+
@@ -466,7 +667,7 @@ -

Contact the administrator in order to receive a valid login.

+

Contact the administrator to receive valid credentials.