Add german translation
This commit is contained in:
parent
80897f0e62
commit
395e1ce5cb
482
dashboard/app.js
482
dashboard/app.js
@ -1,5 +1,8 @@
|
||||
class DashboardApp {
|
||||
constructor() {
|
||||
this.locale = this.resolveLocale();
|
||||
this.translations = this.buildTranslations();
|
||||
|
||||
this.elements = {
|
||||
loginOverlay: document.getElementById('loginOverlay'),
|
||||
loginForm: document.getElementById('loginForm'),
|
||||
@ -72,6 +75,8 @@ class DashboardApp {
|
||||
{ key: 'digitalInputMode', label: 'Digital input' }
|
||||
];
|
||||
|
||||
this.translateDocument();
|
||||
this.updateEasterEggScore();
|
||||
this.renderStaticTemplates();
|
||||
this.attachEventListeners();
|
||||
this.initializePanelNavigation();
|
||||
@ -80,6 +85,351 @@ class DashboardApp {
|
||||
this.updateCarSelector();
|
||||
}
|
||||
|
||||
resolveLocale() {
|
||||
const overrideKey = 'evdash.language';
|
||||
try {
|
||||
const stored = window.localStorage.getItem(overrideKey);
|
||||
if (stored && typeof stored === 'string')
|
||||
return stored.trim().toLowerCase().startsWith('de') ? 'de' : 'en';
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const candidates = Array.isArray(navigator.languages) && navigator.languages.length
|
||||
? navigator.languages
|
||||
: [navigator.language || 'en'];
|
||||
|
||||
const normalized = candidates
|
||||
.filter(Boolean)
|
||||
.map(value => String(value).toLowerCase());
|
||||
|
||||
if (normalized.some(value => value === 'de' || value.startsWith('de-')))
|
||||
return 'de';
|
||||
|
||||
return 'en';
|
||||
}
|
||||
|
||||
buildTranslations() {
|
||||
return {
|
||||
en: {
|
||||
'header.tagline': 'Monitor & troubleshoot EV chargers.',
|
||||
'header.awaitingLogin': 'Awaiting login…',
|
||||
'header.authenticateHint': 'Load the dashboard to authenticate.',
|
||||
'header.logout': 'Logout',
|
||||
|
||||
'sidebar.dashboardSections': 'Dashboard sections',
|
||||
'sidebar.sections': 'Sections',
|
||||
'sidebar.workspace': 'Workspace',
|
||||
'sidebar.overview': 'Overview',
|
||||
'sidebar.chargers': 'Chargers',
|
||||
'sidebar.chargersSubtitle': 'Live table & telemetry',
|
||||
'sidebar.sessions': 'Charging sessions',
|
||||
'sidebar.sessionsSubtitle': 'History of charging sessions',
|
||||
'sidebar.help': 'Help',
|
||||
'sidebar.helpSubtitle': 'API concept & debug logs',
|
||||
'sidebar.version': 'Version',
|
||||
|
||||
'chargers.liveOverview': 'Live overview',
|
||||
'chargers.title': 'Chargers',
|
||||
'chargers.empty': 'No chargers loaded yet.',
|
||||
'chargers.columns.name': 'Name',
|
||||
'chargers.columns.car': 'Car',
|
||||
'chargers.columns.energyManagerMode': 'Energy manager mode',
|
||||
'chargers.columns.connected': 'Connected',
|
||||
'chargers.columns.status': 'Status',
|
||||
'chargers.columns.chargingCurrent': 'Charging current',
|
||||
'chargers.columns.chargingPhases': 'Charging phases',
|
||||
'chargers.columns.currentPower': 'Current power',
|
||||
'chargers.columns.sessionEnergy': 'Session energy',
|
||||
'chargers.columns.version': 'Version',
|
||||
'chargers.columns.temperature': 'Temperature',
|
||||
'chargers.columns.digitalInputMode': 'Digital input',
|
||||
|
||||
'sessions.history': 'History',
|
||||
'sessions.title': 'Charging sessions',
|
||||
'sessions.filterCar': 'Car',
|
||||
'sessions.allCars': 'All cars',
|
||||
'sessions.filterStartDate': 'Start date',
|
||||
'sessions.filterEndDate': 'End date',
|
||||
'sessions.fetch': 'Fetch sessions',
|
||||
'sessions.downloadCsv': 'Download CSV',
|
||||
'sessions.helper': 'Optionally filter charging sessions by car and time range before downloading.',
|
||||
'sessions.columns.name': 'Name',
|
||||
'sessions.columns.charger': 'Charger',
|
||||
'sessions.columns.car': 'Car',
|
||||
'sessions.columns.start': 'Start',
|
||||
'sessions.columns.end': 'End',
|
||||
'sessions.columns.energy': 'Energy (kWh)',
|
||||
'sessions.emptyFetched': 'No charging sessions fetched yet.',
|
||||
'sessions.noneFound': 'No charging sessions found.',
|
||||
'sessions.noneInRange': 'No charging sessions match the selected time range.',
|
||||
'sessions.fetchFailed': 'Failed to fetch charging sessions.',
|
||||
'sessions.requestFailed': 'Unable to request charging sessions. Check the connection status.',
|
||||
'sessions.displayFailed': 'Unable to display charging sessions.',
|
||||
'sessions.startBeforeEnd': 'Start date must be earlier than end date.',
|
||||
'sessions.sessionIdLabel': 'Session {id}',
|
||||
|
||||
'help.guides': 'Guides',
|
||||
'help.title': 'API Contract',
|
||||
'help.helper': 'Use <code>app.sendAction(action, payload)</code> after authenticating.',
|
||||
'help.requestTemplate': 'Request template',
|
||||
'help.responses': 'Responses',
|
||||
'help.diagnostics': 'Diagnostics',
|
||||
'help.lastMessage': 'Last WebSocket message',
|
||||
'help.diagnosticsHelper': 'Sign in to reuse the stored session and inspect backend payloads.',
|
||||
'help.noMessagesYet': 'No messages received yet.',
|
||||
'help.debugging': 'Debugging',
|
||||
'help.sessionsPayload': 'Charging sessions payload',
|
||||
'help.sessionsPayloadHelper': 'Raw session JSON for troubleshooting.',
|
||||
'help.reference': 'Reference',
|
||||
'help.chargerTableBasics': 'Charger table basics',
|
||||
'help.referenceBullet1': 'The dashboard keeps one row per charger ID and updates it with backend notifications.',
|
||||
'help.referenceBullet2': 'Columns follow the order defined by <code>EvDashEngine::packCharger</code> so new properties show up automatically.',
|
||||
'help.referenceBullet3': 'Branding (colours, fonts) is managed via CSS variables at the top of this file for easy overrides.',
|
||||
|
||||
'easterEgg.hiddenTreat': 'Hidden treat',
|
||||
'easterEgg.title': 'Grid Dash',
|
||||
'easterEgg.close': 'Close',
|
||||
'easterEgg.instructions': 'Use arrow keys or WASD to drive the tiny EV and catch lightning bolts. Press Esc or close to exit.',
|
||||
'easterEgg.score': 'Score: {score}',
|
||||
'easterEgg.hint': 'Stay inside the grid!',
|
||||
'easterEgg.canvasLabel': 'Mini game canvas',
|
||||
|
||||
'login.title': 'Sign in',
|
||||
'login.required': 'Authorization required.',
|
||||
'login.username': 'Username',
|
||||
'login.password': 'Password',
|
||||
'login.helper': 'Contact the administrator to receive valid credentials.',
|
||||
'login.signIn': 'Sign in',
|
||||
'login.signingIn': 'Signing in…',
|
||||
'login.emptyCredentials': 'Username and password are required.',
|
||||
'login.failed': 'Login failed. Please try again.',
|
||||
'login.networkError': 'Unable to reach the login endpoint. Please check your connection.',
|
||||
'login.unexpectedResponse': 'Received an unexpected response from the server.',
|
||||
'login.invalidResponse': 'Invalid response from server.',
|
||||
'login.invalidRequest': 'The login request was malformed. Please reload the page and try again.',
|
||||
'login.unauthorized': 'The provided credentials were not accepted.',
|
||||
|
||||
'connection.connecting': 'Connecting…',
|
||||
'connection.authenticating': 'Authenticating…',
|
||||
'connection.connected': 'Connected',
|
||||
'connection.disconnected': 'Disconnected',
|
||||
'connection.error': 'Connection error',
|
||||
'connection.authenticationRequired': 'Authentication required',
|
||||
'connection.loggedOut': 'Logged out',
|
||||
'connection.sessionExpired': 'Session expired. Please sign in again.',
|
||||
'connection.sessionExpiredRestore': 'Your session has expired. Please sign in again.',
|
||||
'connection.restoreFailed': 'We could not restore your previous session. Please sign in again.',
|
||||
'connection.loggedOutOverlay': 'You have been logged out.',
|
||||
'connection.authFailed': 'Authentication failed. Please try again.',
|
||||
|
||||
'value.true': 'True',
|
||||
'value.false': 'False',
|
||||
'value.yes': 'Yes',
|
||||
'value.no': 'No',
|
||||
'value.unknownWithValue': 'Unknown ({value})',
|
||||
|
||||
'energyManagerMode.quick': 'Quick',
|
||||
'energyManagerMode.eco': 'Eco',
|
||||
'energyManagerMode.ecoTime': 'Eco + Time',
|
||||
|
||||
'digitalInputMode.chargingAllowed': 'Charging allowed',
|
||||
'digitalInputMode.chargingAllowedInverted': 'Charging allowed inverted',
|
||||
'digitalInputMode.pwmAndS0': 'PWM and S0 signaling',
|
||||
'digitalInputMode.limitAndS0': 'Limit and S0 signaling',
|
||||
|
||||
'csv.sessionId': 'Session ID',
|
||||
'csv.chargerName': 'Charger name',
|
||||
'csv.chargerSerialNumber': 'Charger serial number',
|
||||
'csv.car': 'Car',
|
||||
'csv.start': 'Start',
|
||||
'csv.end': 'End',
|
||||
'csv.energyKwh': 'Energy [kWh]',
|
||||
'csv.meterStartKwh': 'Meter start [kWh]',
|
||||
'csv.meterEndKwh': 'Meter end [kWh]',
|
||||
|
||||
'ws.parseFailed': 'Failed to parse message: {error}'
|
||||
},
|
||||
de: {
|
||||
'header.tagline': 'Überwachen & Fehleranalyse von EV-Ladestationen.',
|
||||
'header.awaitingLogin': 'Warte auf Anmeldung…',
|
||||
'header.authenticateHint': 'Dashboard laden, um dich zu authentifizieren.',
|
||||
'header.logout': 'Abmelden',
|
||||
|
||||
'sidebar.dashboardSections': 'Dashboard-Bereiche',
|
||||
'sidebar.sections': 'Bereiche',
|
||||
'sidebar.workspace': 'Arbeitsbereich',
|
||||
'sidebar.overview': 'Übersicht',
|
||||
'sidebar.chargers': 'Ladestationen',
|
||||
'sidebar.chargersSubtitle': 'Live-Tabelle & Telemetrie',
|
||||
'sidebar.sessions': 'Ladevorgänge',
|
||||
'sidebar.sessionsSubtitle': 'Historie der Ladevorgänge',
|
||||
'sidebar.help': 'Hilfe',
|
||||
'sidebar.helpSubtitle': 'API-Konzept & Debug-Logs',
|
||||
'sidebar.version': 'Version',
|
||||
|
||||
'chargers.liveOverview': 'Live-Übersicht',
|
||||
'chargers.title': 'Ladestationen',
|
||||
'chargers.empty': 'Noch keine Ladestationen geladen.',
|
||||
'chargers.columns.name': 'Name',
|
||||
'chargers.columns.car': 'Fahrzeug',
|
||||
'chargers.columns.energyManagerMode': 'Energiemanager-Modus',
|
||||
'chargers.columns.connected': 'Verbunden',
|
||||
'chargers.columns.status': 'Status',
|
||||
'chargers.columns.chargingCurrent': 'Ladestrom',
|
||||
'chargers.columns.chargingPhases': 'Ladephasen',
|
||||
'chargers.columns.currentPower': 'Aktuelle Leistung',
|
||||
'chargers.columns.sessionEnergy': 'Energie (Sitzung)',
|
||||
'chargers.columns.version': 'Version',
|
||||
'chargers.columns.temperature': 'Temperatur',
|
||||
'chargers.columns.digitalInputMode': 'Digitaler Eingang',
|
||||
|
||||
'sessions.history': 'Historie',
|
||||
'sessions.title': 'Ladevorgänge',
|
||||
'sessions.filterCar': 'Fahrzeug',
|
||||
'sessions.allCars': 'Alle Fahrzeuge',
|
||||
'sessions.filterStartDate': 'Startdatum',
|
||||
'sessions.filterEndDate': 'Enddatum',
|
||||
'sessions.fetch': 'Ladevorgänge laden',
|
||||
'sessions.downloadCsv': 'CSV herunterladen',
|
||||
'sessions.helper': 'Optional nach Fahrzeug und Zeitraum filtern, bevor du die CSV herunterlädst.',
|
||||
'sessions.columns.name': 'Name',
|
||||
'sessions.columns.charger': 'Ladestation',
|
||||
'sessions.columns.car': 'Fahrzeug',
|
||||
'sessions.columns.start': 'Start',
|
||||
'sessions.columns.end': 'Ende',
|
||||
'sessions.columns.energy': 'Energie (kWh)',
|
||||
'sessions.emptyFetched': 'Noch keine Ladevorgänge geladen.',
|
||||
'sessions.noneFound': 'Keine Ladevorgänge gefunden.',
|
||||
'sessions.noneInRange': 'Keine Ladevorgänge im ausgewählten Zeitraum.',
|
||||
'sessions.fetchFailed': 'Ladevorgänge konnten nicht geladen werden.',
|
||||
'sessions.requestFailed': 'Ladevorgänge konnten nicht angefragt werden. Bitte Verbindungsstatus prüfen.',
|
||||
'sessions.displayFailed': 'Ladevorgänge können nicht angezeigt werden.',
|
||||
'sessions.startBeforeEnd': 'Das Startdatum muss vor dem Enddatum liegen.',
|
||||
'sessions.sessionIdLabel': 'Sitzung {id}',
|
||||
|
||||
'help.guides': 'Leitfäden',
|
||||
'help.title': 'API-Vertrag',
|
||||
'help.helper': 'Nach der Authentifizierung kannst du <code>app.sendAction(action, payload)</code> verwenden.',
|
||||
'help.requestTemplate': 'Request-Vorlage',
|
||||
'help.responses': 'Antworten',
|
||||
'help.diagnostics': 'Diagnose',
|
||||
'help.lastMessage': 'Letzte WebSocket-Nachricht',
|
||||
'help.diagnosticsHelper': 'Anmelden, um die gespeicherte Sitzung zu nutzen und Backend-Payloads zu prüfen.',
|
||||
'help.noMessagesYet': 'Noch keine Nachrichten empfangen.',
|
||||
'help.debugging': 'Debugging',
|
||||
'help.sessionsPayload': 'Ladevorgänge-Payload',
|
||||
'help.sessionsPayloadHelper': 'Rohes Session-JSON zur Fehlersuche.',
|
||||
'help.reference': 'Referenz',
|
||||
'help.chargerTableBasics': 'Grundlagen der Ladestationen-Tabelle',
|
||||
'help.referenceBullet1': 'Das Dashboard hält eine Zeile pro Ladestations-ID und aktualisiert sie über Backend-Benachrichtigungen.',
|
||||
'help.referenceBullet2': 'Die Spalten folgen der Reihenfolge aus <code>EvDashEngine::packCharger</code>, sodass neue Eigenschaften automatisch erscheinen.',
|
||||
'help.referenceBullet3': 'Branding (Farben, Schrift) wird über CSS-Variablen am Anfang dieser Datei gesteuert.',
|
||||
|
||||
'easterEgg.hiddenTreat': 'Verstecktes Extra',
|
||||
'easterEgg.title': 'Grid Dash',
|
||||
'easterEgg.close': 'Schließen',
|
||||
'easterEgg.instructions': 'Mit Pfeiltasten oder WASD fahren, Blitze einsammeln. Mit Esc oder Schließen beenden.',
|
||||
'easterEgg.score': 'Punkte: {score}',
|
||||
'easterEgg.hint': 'Bleib im Raster!',
|
||||
'easterEgg.canvasLabel': 'Mini-Spiel-Leinwand',
|
||||
|
||||
'login.title': 'Anmelden',
|
||||
'login.required': 'Autorisierung erforderlich.',
|
||||
'login.username': 'Benutzername',
|
||||
'login.password': 'Passwort',
|
||||
'login.helper': 'Wende dich an den Administrator, um gültige Zugangsdaten zu erhalten.',
|
||||
'login.signIn': 'Anmelden',
|
||||
'login.signingIn': 'Anmeldung…',
|
||||
'login.emptyCredentials': 'Benutzername und Passwort sind erforderlich.',
|
||||
'login.failed': 'Anmeldung fehlgeschlagen. Bitte erneut versuchen.',
|
||||
'login.networkError': 'Login-Endpunkt nicht erreichbar. Bitte Verbindung prüfen.',
|
||||
'login.unexpectedResponse': 'Unerwartete Antwort vom Server erhalten.',
|
||||
'login.invalidResponse': 'Ungültige Serverantwort.',
|
||||
'login.invalidRequest': 'Die Login-Anfrage war fehlerhaft. Bitte Seite neu laden und erneut versuchen.',
|
||||
'login.unauthorized': 'Die eingegebenen Zugangsdaten wurden nicht akzeptiert.',
|
||||
|
||||
'connection.connecting': 'Verbinden…',
|
||||
'connection.authenticating': 'Authentifizierung…',
|
||||
'connection.connected': 'Verbunden',
|
||||
'connection.disconnected': 'Getrennt',
|
||||
'connection.error': 'Verbindungsfehler',
|
||||
'connection.authenticationRequired': 'Authentifizierung erforderlich',
|
||||
'connection.loggedOut': 'Abgemeldet',
|
||||
'connection.sessionExpired': 'Sitzung abgelaufen. Bitte erneut anmelden.',
|
||||
'connection.sessionExpiredRestore': 'Deine Sitzung ist abgelaufen. Bitte erneut anmelden.',
|
||||
'connection.restoreFailed': 'Die vorige Sitzung konnte nicht wiederhergestellt werden. Bitte erneut anmelden.',
|
||||
'connection.loggedOutOverlay': 'Du wurdest abgemeldet.',
|
||||
'connection.authFailed': 'Authentifizierung fehlgeschlagen. Bitte erneut versuchen.',
|
||||
|
||||
'value.true': 'Wahr',
|
||||
'value.false': 'Falsch',
|
||||
'value.yes': 'Ja',
|
||||
'value.no': 'Nein',
|
||||
'value.unknownWithValue': 'Unbekannt ({value})',
|
||||
|
||||
'energyManagerMode.quick': 'Schnell',
|
||||
'energyManagerMode.eco': 'Eco',
|
||||
'energyManagerMode.ecoTime': 'Eco + Zeit',
|
||||
|
||||
'digitalInputMode.chargingAllowed': 'Laden erlaubt',
|
||||
'digitalInputMode.chargingAllowedInverted': 'Laden erlaubt (invertiert)',
|
||||
'digitalInputMode.pwmAndS0': 'PWM- und S0-Signalisierung',
|
||||
'digitalInputMode.limitAndS0': 'Limit- und S0-Signalisierung',
|
||||
|
||||
'csv.sessionId': 'Sitzungs-ID',
|
||||
'csv.chargerName': 'Ladestationsname',
|
||||
'csv.chargerSerialNumber': 'Seriennummer der Ladestation',
|
||||
'csv.car': 'Fahrzeug',
|
||||
'csv.start': 'Start',
|
||||
'csv.end': 'Ende',
|
||||
'csv.energyKwh': 'Energie [kWh]',
|
||||
'csv.meterStartKwh': 'Zählerstand Start [kWh]',
|
||||
'csv.meterEndKwh': 'Zählerstand Ende [kWh]',
|
||||
|
||||
'ws.parseFailed': 'Nachricht konnte nicht gelesen werden: {error}'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
t(key, variables) {
|
||||
const locale = this.locale in this.translations ? this.locale : 'en';
|
||||
const table = this.translations[locale] || {};
|
||||
const fallback = this.translations.en || {};
|
||||
let text = (key && key in table) ? table[key] : (key in fallback ? fallback[key] : String(key));
|
||||
|
||||
if (variables && typeof variables === 'object') {
|
||||
Object.entries(variables).forEach(([name, value]) => {
|
||||
text = text.replaceAll(`{${name}}`, value === undefined || value === null ? '' : String(value));
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
translateDocument() {
|
||||
try {
|
||||
document.documentElement.lang = this.locale;
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const nodes = document.querySelectorAll('[data-i18n]');
|
||||
nodes.forEach(node => {
|
||||
const key = node.dataset.i18n;
|
||||
if (!key)
|
||||
return;
|
||||
const attr = node.dataset.i18nAttr;
|
||||
const text = this.t(key);
|
||||
if (attr)
|
||||
node.setAttribute(attr, text);
|
||||
else if (node.dataset.i18nMode === 'html')
|
||||
node.innerHTML = text;
|
||||
else
|
||||
node.textContent = text;
|
||||
});
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
if (this.elements.loginForm) {
|
||||
this.elements.loginForm.addEventListener('submit', event => {
|
||||
@ -292,7 +642,7 @@ class DashboardApp {
|
||||
const expiresAt = new Date(parsed.expiresAt);
|
||||
if (Number.isNaN(expiresAt.getTime()) || expiresAt <= new Date()) {
|
||||
this.clearSession();
|
||||
this.showLoginOverlay('Your session has expired. Please sign in again.');
|
||||
this.showLoginOverlay(this.t('connection.sessionExpiredRestore'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -300,13 +650,13 @@ class DashboardApp {
|
||||
this.tokenExpiry = expiresAt;
|
||||
this.username = parsed.username || null;
|
||||
this.scheduleTokenRefresh();
|
||||
this.updateSessionUser();
|
||||
this.hideLoginOverlay();
|
||||
this.connectWebSocket();
|
||||
this.updateSessionUser();
|
||||
this.hideLoginOverlay();
|
||||
this.connectWebSocket();
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore session', error);
|
||||
this.clearSession();
|
||||
this.showLoginOverlay('We could not restore your previous session. Please sign in again.');
|
||||
this.showLoginOverlay(this.t('connection.restoreFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -318,7 +668,7 @@ class DashboardApp {
|
||||
const password = this.elements.password.value;
|
||||
|
||||
if (!username || !password) {
|
||||
this.showLoginError('Username and password are required.');
|
||||
this.showLoginError(this.t('login.emptyCredentials'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -331,7 +681,7 @@ class DashboardApp {
|
||||
this.connectWebSocket(true);
|
||||
})
|
||||
.catch(error => {
|
||||
const message = error && error.message ? error.message : 'Login failed. Please try again.';
|
||||
const message = error && error.message ? error.message : this.t('login.failed');
|
||||
this.showLoginError(message);
|
||||
})
|
||||
.finally(() => {
|
||||
@ -353,7 +703,7 @@ class DashboardApp {
|
||||
});
|
||||
} catch (networkError) {
|
||||
console.warn('Login request failed', networkError);
|
||||
throw new Error('Unable to reach the login endpoint. Please check your connection.');
|
||||
throw new Error(this.t('login.networkError'));
|
||||
}
|
||||
|
||||
let data;
|
||||
@ -361,7 +711,7 @@ class DashboardApp {
|
||||
data = await response.json();
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse login response', parseError);
|
||||
throw new Error('Received an unexpected response from the server.');
|
||||
throw new Error(this.t('login.unexpectedResponse'));
|
||||
}
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
@ -370,7 +720,7 @@ class DashboardApp {
|
||||
}
|
||||
|
||||
if (!data.token || !data.expiresAt)
|
||||
throw new Error('Invalid response from server.');
|
||||
throw new Error(this.t('login.invalidResponse'));
|
||||
|
||||
return {
|
||||
token: data.token,
|
||||
@ -381,11 +731,11 @@ class DashboardApp {
|
||||
describeLoginError(code) {
|
||||
switch (code) {
|
||||
case 'invalidRequest':
|
||||
return 'The login request was malformed. Please reload the page and try again.';
|
||||
return this.t('login.invalidRequest');
|
||||
case 'unauthorized':
|
||||
return 'The provided credentials were not accepted.';
|
||||
return this.t('login.unauthorized');
|
||||
default:
|
||||
return 'Login failed. Please try again.';
|
||||
return this.t('login.failed');
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,7 +771,7 @@ class DashboardApp {
|
||||
this.cars.clear();
|
||||
this.resetChargerTable();
|
||||
this.updateCarSelector();
|
||||
this.renderChargingSessions([], 'No charging sessions fetched yet.');
|
||||
this.renderChargingSessions([], this.t('sessions.emptyFetched'));
|
||||
|
||||
try {
|
||||
window.localStorage.removeItem(this.sessionKey);
|
||||
@ -434,13 +784,13 @@ class DashboardApp {
|
||||
|
||||
connectWebSocket(resetPending = false) {
|
||||
if (!this.token) {
|
||||
this.updateConnectionStatus('Awaiting login…', 'connecting');
|
||||
this.updateConnectionStatus(this.t('header.awaitingLogin'), 'connecting');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tokenExpiry && this.tokenExpiry <= new Date()) {
|
||||
this.clearSession();
|
||||
this.showLoginOverlay('Your session has expired. Please sign in again.');
|
||||
this.showLoginOverlay(this.t('connection.sessionExpiredRestore'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -451,7 +801,7 @@ class DashboardApp {
|
||||
this.pendingRequests.clear();
|
||||
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.updateConnectionStatus('Connecting…', 'connecting');
|
||||
this.updateConnectionStatus(this.t('connection.connecting'), 'connecting');
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const host = window.location.hostname || 'localhost';
|
||||
const port = 4449;
|
||||
@ -460,7 +810,7 @@ class DashboardApp {
|
||||
|
||||
this.socket = new WebSocket(url);
|
||||
this.socket.addEventListener('open', () => {
|
||||
this.updateConnectionStatus('Authenticating…', 'authenticating');
|
||||
this.updateConnectionStatus(this.t('connection.authenticating'), 'authenticating');
|
||||
this.sendAuthenticate();
|
||||
});
|
||||
|
||||
@ -469,7 +819,7 @@ class DashboardApp {
|
||||
});
|
||||
|
||||
this.socket.addEventListener('error', () => {
|
||||
this.updateConnectionStatus('Connection error', 'error');
|
||||
this.updateConnectionStatus(this.t('connection.error'), 'error');
|
||||
});
|
||||
|
||||
this.socket.addEventListener('close', () => {
|
||||
@ -558,12 +908,12 @@ class DashboardApp {
|
||||
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.');
|
||||
this.renderChargingSessions(sessions, this.t('sessions.noneFound'));
|
||||
} else if (data.error === 'unauthenticated') {
|
||||
this.onAuthenticationFailed('unauthenticated');
|
||||
} else {
|
||||
console.warn('GetChargingSessions request failed', data.error || 'unknownError');
|
||||
this.renderChargingSessions([], 'Failed to fetch charging sessions.');
|
||||
this.renderChargingSessions([], this.t('sessions.fetchFailed'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -641,7 +991,7 @@ class DashboardApp {
|
||||
}
|
||||
|
||||
onAuthenticationSucceeded() {
|
||||
this.updateConnectionStatus('Connected', 'connected');
|
||||
this.updateConnectionStatus(this.t('connection.connected'), 'connected');
|
||||
this.updateSessionUser();
|
||||
this.sendGetCars();
|
||||
this.sendGetChargers();
|
||||
@ -650,13 +1000,13 @@ class DashboardApp {
|
||||
|
||||
onAuthenticationFailed(reason) {
|
||||
const message = reason === 'unauthenticated'
|
||||
? 'Your session expired. Please sign in again.'
|
||||
: 'Authentication failed. Please try again.';
|
||||
? this.t('connection.sessionExpired')
|
||||
: this.t('connection.authFailed');
|
||||
|
||||
console.warn('Authentication failed', reason);
|
||||
this.clearSession();
|
||||
this.showLoginOverlay(message);
|
||||
this.updateConnectionStatus('Authentication required', 'error');
|
||||
this.updateConnectionStatus(this.t('connection.authenticationRequired'), 'error');
|
||||
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN)
|
||||
this.socket.close();
|
||||
@ -664,7 +1014,7 @@ class DashboardApp {
|
||||
|
||||
onSocketClosed() {
|
||||
this.pendingRequests.clear();
|
||||
this.updateConnectionStatus('Disconnected', 'error');
|
||||
this.updateConnectionStatus(this.t('connection.disconnected'), 'error');
|
||||
if (!this.token) {
|
||||
this.showLoginOverlay();
|
||||
return;
|
||||
@ -729,7 +1079,7 @@ class DashboardApp {
|
||||
|
||||
const requestId = this.sendAction('GetChargingSessions', payload);
|
||||
if (!requestId)
|
||||
this.renderChargingSessions([], 'Unable to request charging sessions. Check the connection status.');
|
||||
this.renderChargingSessions([], this.t('sessions.requestFailed'));
|
||||
|
||||
return requestId;
|
||||
}
|
||||
@ -816,11 +1166,11 @@ class DashboardApp {
|
||||
const dot = document.createElement('span');
|
||||
dot.className = `value-dot ${value ? 'value-dot-true' : 'value-dot-false'}`;
|
||||
dot.setAttribute('role', 'img');
|
||||
dot.setAttribute('aria-label', value ? 'True' : 'False');
|
||||
dot.title = value ? 'True' : 'False';
|
||||
dot.setAttribute('aria-label', value ? this.t('value.true') : this.t('value.false'));
|
||||
dot.title = value ? this.t('value.true') : this.t('value.false');
|
||||
const srText = document.createElement('span');
|
||||
srText.className = 'sr-only';
|
||||
srText.textContent = value ? 'True' : 'False';
|
||||
srText.textContent = value ? this.t('value.true') : this.t('value.false');
|
||||
cell.appendChild(dot);
|
||||
cell.appendChild(srText);
|
||||
return;
|
||||
@ -959,7 +1309,7 @@ class DashboardApp {
|
||||
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.value = '';
|
||||
defaultOption.textContent = 'All cars';
|
||||
defaultOption.textContent = this.t('sessions.allCars');
|
||||
select.appendChild(defaultOption);
|
||||
|
||||
const cars = Array.from(this.cars.values())
|
||||
@ -993,25 +1343,25 @@ class DashboardApp {
|
||||
|
||||
if (key === 'energyManagerMode') {
|
||||
const modes = {
|
||||
0: 'Quick',
|
||||
1: 'Eco',
|
||||
2: 'Eco + Time'
|
||||
0: this.t('energyManagerMode.quick'),
|
||||
1: this.t('energyManagerMode.eco'),
|
||||
2: this.t('energyManagerMode.ecoTime')
|
||||
};
|
||||
if (value in modes)
|
||||
return modes[value];
|
||||
return Number.isFinite(value) ? `Unknown (${value})` : '—';
|
||||
return Number.isFinite(value) ? this.t('value.unknownWithValue', { value }) : '—';
|
||||
}
|
||||
|
||||
if (key === 'digitalInputMode') {
|
||||
const modes = {
|
||||
0: 'Charging allowed',
|
||||
1: 'Charging allowed inverted',
|
||||
2: 'PWM and S0 signaling',
|
||||
3: 'Limit and S0 signaling'
|
||||
0: this.t('digitalInputMode.chargingAllowed'),
|
||||
1: this.t('digitalInputMode.chargingAllowedInverted'),
|
||||
2: this.t('digitalInputMode.pwmAndS0'),
|
||||
3: this.t('digitalInputMode.limitAndS0')
|
||||
};
|
||||
if (value in modes)
|
||||
return modes[value];
|
||||
return Number.isFinite(value) ? `Unknown (${value})` : '—';
|
||||
return Number.isFinite(value) ? this.t('value.unknownWithValue', { value }) : '—';
|
||||
}
|
||||
|
||||
if ((key === 'currentPower' || key === 'sessionEnergy') && typeof value === 'number') {
|
||||
@ -1023,7 +1373,7 @@ class DashboardApp {
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean')
|
||||
return value ? 'Yes' : 'No';
|
||||
return value ? this.t('value.yes') : this.t('value.no');
|
||||
|
||||
if (typeof value === 'number')
|
||||
return this.formatNumber(value);
|
||||
@ -1049,7 +1399,7 @@ class DashboardApp {
|
||||
return;
|
||||
|
||||
if (!normalizedSessions.length) {
|
||||
this.elements.chargingSessionsOutput.textContent = fallbackMessage || 'No charging sessions found.';
|
||||
this.elements.chargingSessionsOutput.textContent = fallbackMessage || this.t('sessions.noneFound');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1057,7 +1407,7 @@ class DashboardApp {
|
||||
this.elements.chargingSessionsOutput.textContent = JSON.stringify(normalizedSessions, null, 2);
|
||||
} catch (error) {
|
||||
console.warn('Failed to render charging sessions', error);
|
||||
this.elements.chargingSessionsOutput.textContent = 'Unable to display charging sessions.';
|
||||
this.elements.chargingSessionsOutput.textContent = this.t('sessions.displayFailed');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1082,11 +1432,11 @@ class DashboardApp {
|
||||
const cell = emptyRow.querySelector('td');
|
||||
if (cell) {
|
||||
if (!normalizedSessions.length) {
|
||||
cell.textContent = fallbackMessage || 'No charging sessions fetched yet.';
|
||||
cell.textContent = fallbackMessage || this.t('sessions.emptyFetched');
|
||||
} else if (hasTimeRangeFilter) {
|
||||
cell.textContent = 'No charging sessions match the selected time range.';
|
||||
cell.textContent = this.t('sessions.noneInRange');
|
||||
} else {
|
||||
cell.textContent = fallbackMessage || 'No charging sessions found.';
|
||||
cell.textContent = fallbackMessage || this.t('sessions.noneFound');
|
||||
}
|
||||
}
|
||||
emptyRow.classList.remove('hidden');
|
||||
@ -1120,7 +1470,7 @@ class DashboardApp {
|
||||
this.elements.sessionEndFilter.setCustomValidity('');
|
||||
|
||||
if (Number.isFinite(startMs) && Number.isFinite(endMs) && startMs > endMs) {
|
||||
const message = 'Start date must be earlier than end date.';
|
||||
const message = this.t('sessions.startBeforeEnd');
|
||||
if (this.elements.sessionStartFilter)
|
||||
this.elements.sessionStartFilter.setCustomValidity(message);
|
||||
if (this.elements.sessionEndFilter)
|
||||
@ -1232,7 +1582,7 @@ class DashboardApp {
|
||||
return session.property;
|
||||
|
||||
if (session.sessionId)
|
||||
return `Session ${session.sessionId}`;
|
||||
return this.t('sessions.sessionIdLabel', { id: session.sessionId });
|
||||
|
||||
return '—';
|
||||
}
|
||||
@ -1314,15 +1664,15 @@ class DashboardApp {
|
||||
return '';
|
||||
|
||||
const columns = [
|
||||
{ label: 'Session ID', key: 'sessionId' },
|
||||
{ label: 'Charger name', key: 'chargerName' },
|
||||
{ label: 'Charger serial number', key: 'chargerSerialNumber' },
|
||||
{ label: 'Car', key: 'carName' },
|
||||
{ label: 'Start', key: 'startTimestamp', formatter: value => this.formatCsvTimestamp(value) },
|
||||
{ label: 'End', key: 'endTimestamp', formatter: value => this.formatCsvTimestamp(value) },
|
||||
{ label: 'Energy [kWh]', key: 'sessionEnergy' },
|
||||
{ label: 'Meter start [kWh]', key: 'energyStart' },
|
||||
{ label: 'Meter end [kWh]', key: 'energyEnd' }
|
||||
{ label: this.t('csv.sessionId'), key: 'sessionId' },
|
||||
{ label: this.t('csv.chargerName'), key: 'chargerName' },
|
||||
{ label: this.t('csv.chargerSerialNumber'), key: 'chargerSerialNumber' },
|
||||
{ label: this.t('csv.car'), key: 'carName' },
|
||||
{ label: this.t('csv.start'), key: 'startTimestamp', formatter: value => this.formatCsvTimestamp(value) },
|
||||
{ label: this.t('csv.end'), key: 'endTimestamp', formatter: value => this.formatCsvTimestamp(value) },
|
||||
{ label: this.t('csv.energyKwh'), key: 'sessionEnergy' },
|
||||
{ label: this.t('csv.meterStartKwh'), key: 'energyStart' },
|
||||
{ label: this.t('csv.meterEndKwh'), key: 'energyEnd' }
|
||||
];
|
||||
|
||||
const lines = [];
|
||||
@ -1657,7 +2007,7 @@ class DashboardApp {
|
||||
updateEasterEggScore() {
|
||||
if (!this.elements.easterEggScore)
|
||||
return;
|
||||
this.elements.easterEggScore.textContent = `Score: ${this.easterEggGame.score}`;
|
||||
this.elements.easterEggScore.textContent = this.t('easterEgg.score', { score: this.easterEggGame.score });
|
||||
}
|
||||
|
||||
sanitizeFilename(value) {
|
||||
@ -1692,8 +2042,8 @@ class DashboardApp {
|
||||
return;
|
||||
|
||||
const defaultLabel = document.body && document.body.dataset.mode === 'help'
|
||||
? 'Load the dashboard to authenticate.'
|
||||
: 'Awaiting login…';
|
||||
? this.t('header.authenticateHint')
|
||||
: this.t('header.awaitingLogin');
|
||||
|
||||
if (!this.token || !this.username) {
|
||||
this.elements.sessionUsername.textContent = defaultLabel;
|
||||
@ -1743,7 +2093,7 @@ class DashboardApp {
|
||||
if (!this.elements.loginButton)
|
||||
return;
|
||||
this.elements.loginButton.disabled = loading;
|
||||
this.elements.loginButton.textContent = loading ? 'Signing in…' : 'Sign in';
|
||||
this.elements.loginButton.textContent = loading ? this.t('login.signingIn') : this.t('login.signIn');
|
||||
}
|
||||
|
||||
generateRequestId() {
|
||||
@ -1770,9 +2120,9 @@ class DashboardApp {
|
||||
this.clearSession();
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN)
|
||||
this.socket.close();
|
||||
this.updateConnectionStatus('Logged out', 'connecting');
|
||||
this.updateConnectionStatus(this.t('connection.loggedOut'), 'connecting');
|
||||
this.updateSessionUser();
|
||||
this.showLoginOverlay('You have been logged out.');
|
||||
this.showLoginOverlay(this.t('connection.loggedOutOverlay'));
|
||||
}
|
||||
|
||||
scheduleTokenRefresh() {
|
||||
@ -1812,7 +2162,7 @@ class DashboardApp {
|
||||
throw new Error(data && data.error ? data.error : 'refreshFailed');
|
||||
|
||||
if (!data.token || !data.expiresAt)
|
||||
throw new Error('Invalid response from server.');
|
||||
throw new Error(this.t('login.invalidResponse'));
|
||||
|
||||
this.persistSession({
|
||||
token: data.token,
|
||||
@ -1823,10 +2173,10 @@ class DashboardApp {
|
||||
} catch (error) {
|
||||
console.warn('Token refresh failed', error);
|
||||
this.clearSession();
|
||||
this.updateConnectionStatus('Authentication required', 'error');
|
||||
this.updateConnectionStatus(this.t('connection.authenticationRequired'), 'error');
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN)
|
||||
this.socket.close();
|
||||
this.showLoginOverlay('Session expired. Please sign in again.');
|
||||
this.showLoginOverlay(this.t('connection.sessionExpired'));
|
||||
} finally {
|
||||
this.refreshInFlight = false;
|
||||
}
|
||||
|
||||
@ -72,11 +72,31 @@
|
||||
<body>
|
||||
<main class="redirect-panel">
|
||||
<img src="styles/pce/icon.svg" alt="EV Dash logo">
|
||||
<h1>Help moved into the dashboard</h1>
|
||||
<p>The help content now lives inside the built-in side panel. You will be redirected automatically.</p>
|
||||
<a href="index.html#help">Open EV Dash</a>
|
||||
<h1 id="redirectTitle">Help moved into the dashboard</h1>
|
||||
<p id="redirectText">The help content now lives inside the built-in side panel. You will be redirected automatically.</p>
|
||||
<a id="redirectLink" href="index.html#help">Open EV Dash</a>
|
||||
</main>
|
||||
<script>
|
||||
const isGerman = (() => {
|
||||
const candidates = Array.isArray(navigator.languages) && navigator.languages.length
|
||||
? navigator.languages
|
||||
: [navigator.language || 'en'];
|
||||
return candidates
|
||||
.filter(Boolean)
|
||||
.map(value => String(value).toLowerCase())
|
||||
.some(value => value === 'de' || value.startsWith('de-'));
|
||||
})();
|
||||
|
||||
if (isGerman) {
|
||||
try { document.documentElement.lang = 'de'; } catch (error) { /* ignore */ }
|
||||
const title = document.getElementById('redirectTitle');
|
||||
const text = document.getElementById('redirectText');
|
||||
const link = document.getElementById('redirectLink');
|
||||
if (title) title.textContent = 'Hilfe wurde ins Dashboard verschoben';
|
||||
if (text) text.textContent = 'Die Hilfe befindet sich jetzt im integrierten Seitenbereich. Du wirst automatisch weitergeleitet.';
|
||||
if (link) link.textContent = 'EV Dash öffnen';
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
try {
|
||||
window.location.replace('index.html#help');
|
||||
|
||||
@ -649,44 +649,44 @@
|
||||
<img src="styles/pce/icon.svg" alt="EV Dash logo" class="brand-logo" id="brandLogo">
|
||||
<div class="brand-text">
|
||||
<h1>EV Dash</h1>
|
||||
<p>Monitor & troubleshoot EV chargers.</p>
|
||||
<p data-i18n="header.tagline">Monitor & troubleshoot EV chargers.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-panel">
|
||||
<p class="status-indicator">
|
||||
<span id="statusDot" class="status-dot connecting" aria-hidden="true"></span>
|
||||
<span id="connectionStatus">Awaiting login…</span>
|
||||
<span id="connectionStatus" data-i18n="header.awaitingLogin">Awaiting login…</span>
|
||||
</p>
|
||||
<div class="session-user">
|
||||
<img src="icons/user.svg" alt="" aria-hidden="true" class="session-user-icon">
|
||||
<span id="sessionUsername">Awaiting login…</span>
|
||||
<span id="sessionUsername" data-i18n="header.awaitingLogin">Awaiting login…</span>
|
||||
</div>
|
||||
<button type="button" id="logoutButton" class="ghost hidden">Logout</button>
|
||||
<button type="button" id="logoutButton" class="ghost hidden" data-i18n="header.logout">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="side-panel" aria-label="Dashboard sections">
|
||||
<aside class="side-panel" aria-label="Dashboard sections" data-i18n="sidebar.dashboardSections" data-i18n-attr="aria-label">
|
||||
<div class="side-panel-header">
|
||||
<p class="eyebrow">Workspace</p>
|
||||
<h2>Overview</h2>
|
||||
<p class="eyebrow" data-i18n="sidebar.workspace">Workspace</p>
|
||||
<h2 data-i18n="sidebar.overview">Overview</h2>
|
||||
</div>
|
||||
<nav class="side-nav" aria-label="Sections">
|
||||
<nav class="side-nav" aria-label="Sections" data-i18n="sidebar.sections" data-i18n-attr="aria-label">
|
||||
<button type="button" class="side-nav-button active" data-panel-target="chargers" aria-pressed="true">
|
||||
<span>Chargers</span>
|
||||
<span class="side-nav-subtitle">Live table & telemetry</span>
|
||||
<span data-i18n="sidebar.chargers">Chargers</span>
|
||||
<span class="side-nav-subtitle" data-i18n="sidebar.chargersSubtitle">Live table & telemetry</span>
|
||||
</button>
|
||||
<button type="button" class="side-nav-button" data-panel-target="chargingSessions" aria-pressed="false">
|
||||
<span>Charging sessions</span>
|
||||
<span class="side-nav-subtitle">History of charging sessions</span>
|
||||
<span data-i18n="sidebar.sessions">Charging sessions</span>
|
||||
<span class="side-nav-subtitle" data-i18n="sidebar.sessionsSubtitle">History of charging sessions</span>
|
||||
</button>
|
||||
<button type="button" class="side-nav-button" data-panel-target="help" aria-pressed="false">
|
||||
<span>Help</span>
|
||||
<span class="side-nav-subtitle">API concept & debug logs</span>
|
||||
<span data-i18n="sidebar.help">Help</span>
|
||||
<span class="side-nav-subtitle" data-i18n="sidebar.helpSubtitle">API concept & debug logs</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="side-panel-footer">
|
||||
Version 1.0.0
|
||||
<span data-i18n="sidebar.version">Version</span> 1.0.0
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -696,31 +696,31 @@
|
||||
<article class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Live overview</p>
|
||||
<h2 id="chargersTitle">Chargers</h2>
|
||||
<p class="eyebrow" data-i18n="chargers.liveOverview">Live overview</p>
|
||||
<h2 id="chargersTitle" data-i18n="chargers.title">Chargers</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table chargers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Car</th>
|
||||
<th scope="col">Energy manager mode</th>
|
||||
<th scope="col">Connected</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Charging current</th>
|
||||
<th scope="col">Charging phases</th>
|
||||
<th scope="col">Current power</th>
|
||||
<th scope="col">Session energy</th>
|
||||
<th scope="col">Version</th>
|
||||
<th scope="col">Temperature</th>
|
||||
<th scope="col">Digital input</th>
|
||||
<th scope="col" data-i18n="chargers.columns.name">Name</th>
|
||||
<th scope="col" data-i18n="chargers.columns.car">Car</th>
|
||||
<th scope="col" data-i18n="chargers.columns.energyManagerMode">Energy manager mode</th>
|
||||
<th scope="col" data-i18n="chargers.columns.connected">Connected</th>
|
||||
<th scope="col" data-i18n="chargers.columns.status">Status</th>
|
||||
<th scope="col" data-i18n="chargers.columns.chargingCurrent">Charging current</th>
|
||||
<th scope="col" data-i18n="chargers.columns.chargingPhases">Charging phases</th>
|
||||
<th scope="col" data-i18n="chargers.columns.currentPower">Current power</th>
|
||||
<th scope="col" data-i18n="chargers.columns.sessionEnergy">Session energy</th>
|
||||
<th scope="col" data-i18n="chargers.columns.version">Version</th>
|
||||
<th scope="col" data-i18n="chargers.columns.temperature">Temperature</th>
|
||||
<th scope="col" data-i18n="chargers.columns.digitalInputMode">Digital input</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chargerTableBody">
|
||||
<tr id="chargerEmptyRow" class="empty-row">
|
||||
<td colspan="12">No chargers loaded yet.</td>
|
||||
<td colspan="12" data-i18n="chargers.empty">No chargers loaded yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -732,38 +732,38 @@
|
||||
<article class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">History</p>
|
||||
<h2 id="chargingSessionsTitle">Charging sessions</h2>
|
||||
<p class="eyebrow" data-i18n="sessions.history">History</p>
|
||||
<h2 id="chargingSessionsTitle" data-i18n="sessions.title">Charging sessions</h2>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<label class="sr-only" for="carFilter">Car</label>
|
||||
<label class="sr-only" for="carFilter" data-i18n="sessions.filterCar">Car</label>
|
||||
<select id="carFilter" name="carFilter">
|
||||
<option value="">All cars</option>
|
||||
<option value="" data-i18n="sessions.allCars">All cars</option>
|
||||
</select>
|
||||
<label class="sr-only" for="sessionStartFilter">Start date</label>
|
||||
<label class="sr-only" for="sessionStartFilter" data-i18n="sessions.filterStartDate">Start date</label>
|
||||
<input type="date" id="sessionStartFilter" name="sessionStartFilter">
|
||||
<label class="sr-only" for="sessionEndFilter">End date</label>
|
||||
<label class="sr-only" for="sessionEndFilter" data-i18n="sessions.filterEndDate">End date</label>
|
||||
<input type="date" id="sessionEndFilter" name="sessionEndFilter">
|
||||
<button type="button" id="fetchSessionsButton" class="primary">Fetch sessions</button>
|
||||
<button type="button" id="downloadSessionsButton">Download CSV</button>
|
||||
<button type="button" id="fetchSessionsButton" class="primary" data-i18n="sessions.fetch">Fetch sessions</button>
|
||||
<button type="button" id="downloadSessionsButton" data-i18n="sessions.downloadCsv">Download CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="helper-text">Optionally filter charging sessions by car and time range before downloading.</p>
|
||||
<p class="helper-text" data-i18n="sessions.helper">Optionally filter charging sessions by car and time range before downloading.</p>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table sessions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Charger</th>
|
||||
<th scope="col">Car</th>
|
||||
<th scope="col">Start</th>
|
||||
<th scope="col">End</th>
|
||||
<th scope="col">Energy (kWh)</th>
|
||||
<th scope="col" data-i18n="sessions.columns.name">Name</th>
|
||||
<th scope="col" data-i18n="sessions.columns.charger">Charger</th>
|
||||
<th scope="col" data-i18n="sessions.columns.car">Car</th>
|
||||
<th scope="col" data-i18n="sessions.columns.start">Start</th>
|
||||
<th scope="col" data-i18n="sessions.columns.end">End</th>
|
||||
<th scope="col" data-i18n="sessions.columns.energy">Energy (kWh)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chargingSessionsTableBody">
|
||||
<tr id="chargingSessionsEmptyRow" class="empty-row">
|
||||
<td colspan="6">No charging sessions fetched yet.</td>
|
||||
<td colspan="6" data-i18n="sessions.emptyFetched">No charging sessions fetched yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -775,18 +775,18 @@
|
||||
<article class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Guides</p>
|
||||
<h2 id="helpTitle">API Contract</h2>
|
||||
<p class="eyebrow" data-i18n="help.guides">Guides</p>
|
||||
<h2 id="helpTitle" data-i18n="help.title">API Contract</h2>
|
||||
</div>
|
||||
<p class="helper-text">Use <code>app.sendAction(action, payload)</code> after authenticating.</p>
|
||||
<p class="helper-text" data-i18n="help.helper" data-i18n-mode="html">Use <code>app.sendAction(action, payload)</code> after authenticating.</p>
|
||||
</div>
|
||||
<div class="grid-two-column">
|
||||
<div>
|
||||
<h3>Request template</h3>
|
||||
<h3 data-i18n="help.requestTemplate">Request template</h3>
|
||||
<pre id="requestTemplate"></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Responses</h3>
|
||||
<h3 data-i18n="help.responses">Responses</h3>
|
||||
<pre id="responseTemplate"></pre>
|
||||
</div>
|
||||
</div>
|
||||
@ -795,36 +795,36 @@
|
||||
<article class="card" aria-live="polite">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Diagnostics</p>
|
||||
<h2>Last WebSocket message</h2>
|
||||
<p class="eyebrow" data-i18n="help.diagnostics">Diagnostics</p>
|
||||
<h2 data-i18n="help.lastMessage">Last WebSocket message</h2>
|
||||
</div>
|
||||
<p class="helper-text">Sign in to reuse the stored session and inspect backend payloads.</p>
|
||||
<p class="helper-text" data-i18n="help.diagnosticsHelper">Sign in to reuse the stored session and inspect backend payloads.</p>
|
||||
</div>
|
||||
<pre id="incomingMessage">No messages received yet.</pre>
|
||||
<pre id="incomingMessage" data-i18n="help.noMessagesYet">No messages received yet.</pre>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Debugging</p>
|
||||
<h2>Charging sessions payload</h2>
|
||||
<p class="eyebrow" data-i18n="help.debugging">Debugging</p>
|
||||
<h2 data-i18n="help.sessionsPayload">Charging sessions payload</h2>
|
||||
</div>
|
||||
<p class="helper-text">Raw session JSON for troubleshooting.</p>
|
||||
<p class="helper-text" data-i18n="help.sessionsPayloadHelper">Raw session JSON for troubleshooting.</p>
|
||||
</div>
|
||||
<pre id="chargingSessionsOutput">No charging sessions fetched yet.</pre>
|
||||
<pre id="chargingSessionsOutput" data-i18n="sessions.emptyFetched">No charging sessions fetched yet.</pre>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Reference</p>
|
||||
<h2>Charger table basics</h2>
|
||||
<p class="eyebrow" data-i18n="help.reference">Reference</p>
|
||||
<h2 data-i18n="help.chargerTableBasics">Charger table basics</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>The dashboard keeps one row per charger ID and updates it with backend notifications.</li>
|
||||
<li>Columns follow the order defined by <code>EvDashEngine::packCharger</code> so new properties show up automatically.</li>
|
||||
<li>Branding (colours, fonts) is managed via CSS variables at the top of this file for easy overrides.</li>
|
||||
<li data-i18n="help.referenceBullet1">The dashboard keeps one row per charger ID and updates it with backend notifications.</li>
|
||||
<li data-i18n="help.referenceBullet2" data-i18n-mode="html">Columns follow the order defined by <code>EvDashEngine::packCharger</code> so new properties show up automatically.</li>
|
||||
<li data-i18n="help.referenceBullet3">Branding (colours, fonts) is managed via CSS variables at the top of this file for easy overrides.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
@ -840,34 +840,34 @@
|
||||
<div class="easter-egg-card" role="dialog" aria-modal="true" aria-labelledby="easterEggTitle">
|
||||
<div class="easter-egg-header">
|
||||
<div>
|
||||
<p class="eyebrow">Hidden treat</p>
|
||||
<h3 id="easterEggTitle">Grid Dash</h3>
|
||||
<p class="eyebrow" data-i18n="easterEgg.hiddenTreat">Hidden treat</p>
|
||||
<h3 id="easterEggTitle" data-i18n="easterEgg.title">Grid Dash</h3>
|
||||
</div>
|
||||
<button type="button" id="easterEggClose" class="ghost">Close</button>
|
||||
<button type="button" id="easterEggClose" class="ghost" data-i18n="easterEgg.close">Close</button>
|
||||
</div>
|
||||
<p class="helper-text">Use arrow keys or WASD to drive the tiny EV and catch lightning bolts. Press Esc or close to exit.</p>
|
||||
<p class="helper-text" data-i18n="easterEgg.instructions">Use arrow keys or WASD to drive the tiny EV and catch lightning bolts. Press Esc or close to exit.</p>
|
||||
<div class="easter-egg-meta">
|
||||
<span id="easterEggScore">Score: 0</span>
|
||||
<span id="easterEggHint">Stay inside the grid!</span>
|
||||
<span id="easterEggHint" data-i18n="easterEgg.hint">Stay inside the grid!</span>
|
||||
</div>
|
||||
<canvas id="easterEggCanvas" width="520" height="320" aria-label="Mini game canvas"></canvas>
|
||||
<canvas id="easterEggCanvas" width="520" height="320" aria-label="Mini game canvas" data-i18n="easterEgg.canvasLabel" data-i18n-attr="aria-label"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="loginOverlay" class="login-view" aria-labelledby="loginTitle">
|
||||
<div class="login-panel">
|
||||
<h2 id="loginTitle">Sign in</h2>
|
||||
<p>Authorization required.</p>
|
||||
<h2 id="loginTitle" data-i18n="login.title">Sign in</h2>
|
||||
<p data-i18n="login.required">Authorization required.</p>
|
||||
<div id="loginError" class="error-message hidden" role="alert"></div>
|
||||
<form id="loginForm" autocomplete="on">
|
||||
<label for="username">Username
|
||||
<label for="username"><span data-i18n="login.username">Username</span>
|
||||
<input type="text" id="username" name="username" autocomplete="username" required placeholder="user">
|
||||
</label>
|
||||
<label for="password">Password
|
||||
<label for="password"><span data-i18n="login.password">Password</span>
|
||||
<input type="password" id="password" name="password" autocomplete="current-password" required placeholder="••••••••">
|
||||
</label>
|
||||
<p class="helper-text">Contact the administrator to receive valid credentials.</p>
|
||||
<button type="submit" id="loginButton" class="primary">Sign in</button>
|
||||
<p class="helper-text" data-i18n="login.helper">Contact the administrator to receive valid credentials.</p>
|
||||
<button type="submit" id="loginButton" class="primary" data-i18n="login.signIn">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user