diff --git a/dashboard/app.js b/dashboard/app.js index 5f3b4da..ecc942a 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -16,6 +16,9 @@ class DashboardApp { incomingMessage: document.getElementById('incomingMessage'), chargerTableBody: document.getElementById('chargerTableBody'), chargerEmptyRow: document.getElementById('chargerEmptyRow'), + fetchSessionsButton: document.getElementById('fetchSessionsButton'), + chargerFilter: document.getElementById('chargerFilter'), + chargingSessionsOutput: document.getElementById('chargingSessionsOutput'), panelButtons: Array.from(document.querySelectorAll('[data-panel-target]')), contentPanels: Array.from(document.querySelectorAll('[data-panel]')) }; @@ -30,6 +33,7 @@ class DashboardApp { this.tokenRefreshTimer = null; this.refreshInFlight = false; this.chargers = new Map(); + this.sessions = []; this.activePanel = null; this.chargerColumns = [ { key: 'id', label: 'ID', hidden: true }, @@ -51,6 +55,7 @@ class DashboardApp { this.initializePanelNavigation(); this.restoreSession(); this.toggleChargerEmptyState(); + this.updateChargerSelector(); } attachEventListeners() { @@ -66,6 +71,12 @@ class DashboardApp { this.logout(); }); } + + if (this.elements.fetchSessionsButton) { + this.elements.fetchSessionsButton.addEventListener('click', () => { + this.fetchChargingSessions(); + }); + } } initializePanelNavigation() { @@ -341,6 +352,7 @@ class DashboardApp { this.refreshInFlight = false; this.chargers.clear(); this.resetChargerTable(); + this.renderChargingSessions([], 'No charging sessions fetched yet.'); try { window.localStorage.removeItem(this.sessionKey); @@ -460,6 +472,20 @@ class DashboardApp { return true; } + if (type === 'getchargingsessions') { + if (data.success) { + const payload = data && data.payload ? data.payload : {}; + const sessions = Array.isArray(payload.sessions) ? payload.sessions : []; + this.renderChargingSessions(sessions, 'No charging sessions found.'); + } else if (data.error === 'unauthenticated') { + this.onAuthenticationFailed('unauthenticated'); + } else { + console.warn('GetChargingSessions request failed', data.error || 'unknownError'); + this.renderChargingSessions([], 'Failed to fetch charging sessions.'); + } + return true; + } + return false; } @@ -480,6 +506,11 @@ class DashboardApp { return true; } + if (Array.isArray(payload.sessions)) { + this.renderChargingSessions(payload.sessions); + return true; + } + if (payload.charger) { this.upsertCharger(payload.charger); return true; @@ -501,6 +532,10 @@ class DashboardApp { case 'chargerremoved': this.removeCharger(payload); return true; + case 'chargingsessionsupdated': + if (payload && Array.isArray(payload.sessions)) + this.renderChargingSessions(payload.sessions); + return true; default: return false; } @@ -581,6 +616,19 @@ class DashboardApp { return this.sendAction('GetChargers', { }); } + fetchChargingSessions() { + const payload = {}; + const chargerId = this.elements.chargerFilter ? this.elements.chargerFilter.value : ''; + if (chargerId) + payload.chargerId = chargerId; + + const requestId = this.sendAction('GetChargingSessions', payload); + if (!requestId) + this.renderChargingSessions([], 'Unable to request charging sessions. Check the connection status.'); + + return requestId; + } + processChargerList(chargers = []) { if (!Array.isArray(chargers)) { console.warn('Expected chargers array in payload.'); @@ -600,6 +648,8 @@ class DashboardApp { if (!seen.has(existingId)) this.removeCharger(existingId); } + + this.updateChargerSelector(); } upsertCharger(charger) { @@ -613,6 +663,7 @@ class DashboardApp { merged.thingId = key; this.chargers.set(key, merged); this.syncChargerRow(merged, !hasExisting); + this.updateChargerSelector(); } syncChargerRow(charger, forceCreate = false) { @@ -687,6 +738,7 @@ class DashboardApp { row.parentElement.removeChild(row); this.toggleChargerEmptyState(); + this.updateChargerSelector(); } resetChargerTable() { @@ -700,6 +752,7 @@ class DashboardApp { }); this.toggleChargerEmptyState(); + this.updateChargerSelector(); } findChargerRow(chargerId) { @@ -736,6 +789,37 @@ class DashboardApp { this.elements.chargerEmptyRow.classList.toggle('hidden', hasChargers); } + updateChargerSelector() { + const select = this.elements.chargerFilter; + if (!select) + return; + + const currentValue = select.value; + while (select.options.length > 0) + select.remove(0); + + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'All chargers'; + select.appendChild(defaultOption); + + const chargers = Array.from(this.chargers.values()) + .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })); + + chargers.forEach(charger => { + const option = document.createElement('option'); + option.value = this.getChargerKey(charger) || ''; + option.textContent = charger.name || option.value; + select.appendChild(option); + }); + + const hasValue = currentValue && select.querySelector + && typeof CSS !== 'undefined' && CSS.escape + && select.querySelector(`option[value="${CSS.escape(currentValue)}"]`); + + select.value = hasValue ? currentValue : ''; + } + formatNumber(value, unit) { if (!Number.isFinite(value)) return '—'; @@ -784,6 +868,25 @@ class DashboardApp { } } + renderChargingSessions(sessions, fallbackMessage) { + if (!this.elements.chargingSessionsOutput) + return; + + if (!Array.isArray(sessions) || !sessions.length) { + this.sessions = []; + this.elements.chargingSessionsOutput.textContent = fallbackMessage || 'No charging sessions found.'; + return; + } + + this.sessions = sessions; + try { + this.elements.chargingSessionsOutput.textContent = JSON.stringify(sessions, null, 2); + } catch (error) { + console.warn('Failed to render charging sessions', error); + this.elements.chargingSessionsOutput.textContent = 'Unable to display charging sessions.'; + } + } + updateConnectionStatus(text, state) { if (this.elements.connectionStatus) this.elements.connectionStatus.textContent = text; diff --git a/dashboard/index.html b/dashboard/index.html index 0565a9e..954b8a5 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -283,6 +283,13 @@ margin-bottom: 1rem; } + .action-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + } + .eyebrow { margin: 0; font-size: 0.8rem; @@ -475,6 +482,22 @@ box-shadow: 0 0 0 3px rgba(162, 13, 23, 0.2); } + select { + border-radius: 10px; + border: 1px solid #d3dce6; + padding: 0.65rem 1rem; + font-size: 1rem; + background: #f9fbfd; + color: var(--text-color); + min-width: 200px; + } + + select:focus { + outline: none; + border-color: var(--secondary-color); + box-shadow: 0 0 0 3px rgba(162, 13, 23, 0.2); + } + button { border: none; border-radius: 999px; @@ -600,6 +623,10 @@ Chargers Live table & telemetry + + + +

Select a charger to filter by its assigned car and fetch sessions from nymea.

+
No charging sessions fetched yet.
+ + +