]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(lint): enable Vue strictTypeChecked and fix config
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 11 May 2026 21:09:48 +0000 (23:09 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 11 May 2026 21:09:48 +0000 (23:09 +0200)
16 files changed:
cspell.config.yaml [new file with mode: 0644]
eslint.config.js
ui/web/src/App.vue
ui/web/src/skins/classic/ClassicLayout.vue
ui/web/src/skins/classic/components/actions/AddChargingStations.vue
ui/web/src/skins/classic/components/actions/SetSupervisionUrl.vue
ui/web/src/skins/classic/components/actions/StartTransaction.vue
ui/web/src/skins/classic/components/buttons/ToggleButton.vue
ui/web/src/skins/classic/components/charging-stations/CSConnector.vue
ui/web/src/skins/classic/components/charging-stations/CSData.vue
ui/web/src/skins/modern/ModernLayout.vue
ui/web/src/skins/modern/components/ConnectorRow.vue
ui/web/src/skins/modern/components/ModernModal.vue
ui/web/src/skins/modern/components/SimulatorBar.vue
ui/web/src/skins/modern/components/StationCard.vue
ui/web/src/skins/modern/components/dialogs/SetSupervisionUrlDialog.vue

diff --git a/cspell.config.yaml b/cspell.config.yaml
new file mode 100644 (file)
index 0000000..d9f74be
--- /dev/null
@@ -0,0 +1,100 @@
+version: '0.2'
+words:
+  - DECI
+  - CENTI
+  - MILLI
+  - MILLIWATT
+  - Benoit
+  - catppuccin
+  - chargingstations
+  - ctrlr
+  - csms
+  - idtag
+  - idtags
+  - iccid
+  - imsi
+  - ocpp
+  - onconnection
+  - opencode
+  - evse
+  - evses
+  - kvar
+  - kvarh
+  - varh
+  - rfid
+  - workerset
+  - worktree
+  - dedup
+  - unpushed
+  - logform
+  - mnemonist
+  - poolifier
+  - measurand
+  - measurands
+  - mikro
+  - neostandard
+  - recurrency
+  - shutdowning
+  - VCAP
+  - workerd
+  - yxxx
+  - cppwm
+  - heartbeatinterval
+  - HEARTBEATINTERVAL
+  - websocketpinginterval
+  - WEBSOCKETPINGINTERVAL
+  - connectionurl
+  - CONNECTIONURL
+  - chargingstation
+  - CHARGINGSTATION
+  - authctrlr
+  - AUTHCTRLR
+  - recloser
+  - deauthorize
+  - Deauth
+  - DEAUTHORIZE
+  - deauthorized
+  - DEAUTHORIZED
+  - Deauthorization
+  - Selftest
+  - SECC
+  - Secc
+  - Overcurrent
+  - ocsp
+  - OCSP
+  - EMAID
+  - emaid
+  - IDTOKEN
+  - idtoken
+  - issuerkeyhash
+  - issuernamehash
+  - SRPC
+  - CALLRESULT
+  - CALLERROR
+  - CALLRESULTERROR
+  - reservability
+  - PPTP
+  - UIMCP
+  - Streamable
+  - modelcontextprotocol
+  - OCMF
+  - ocmf
+  - secp
+  - brainpool
+  - Eichrecht
+  - eichrecht
+  - signingmethod
+  - SIGNINGMETHOD
+  - encodingmethod
+  - ENCODINGMETHOD
+  - signedmeterdata
+  - SIGNEDMETERDATA
+  - fiscalmetering
+  - FISCALMETERING
+  - publickeywithsignedmetervalue
+  - PUBLICKEYWITHSIGNEDMETERVALUE
+  - sampleddatasignreadings
+  - SAMPLEDDATASIGNREADINGS
+  - focusables
+  - Focusables
+  - secret
index 0a32bbc966bb6d6c28138a8f698495b852185a0f..601875cf3bbace53cd267537a20150e3b7598e6b 100644 (file)
@@ -6,129 +6,23 @@ import perfectionist from 'eslint-plugin-perfectionist'
 import pluginVue from 'eslint-plugin-vue'
 import { defineConfig } from 'eslint/config'
 import neostandard, { plugins } from 'neostandard'
+import vueParser from 'vue-eslint-parser'
+
+const GLOB_TS = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts']
+const GLOB_TS_VUE = [...GLOB_TS, '**/*.vue']
+const GLOB_JS = ['**/*.js', '**/*.mjs', '**/*.cjs']
 
 export default defineConfig([
   {
     ignores: ['**/dist/**'],
   },
+
+  // Base configs
+
   cspellConfigs.recommended,
   {
     rules: {
-      '@cspell/spellchecker': [
-        'warn',
-        {
-          autoFix: true,
-          cspell: {
-            words: [
-              'DECI',
-              'CENTI',
-              'MILLI',
-              'MILLIWATT',
-              'Benoit',
-              'catppuccin',
-              'chargingstations',
-              'ctrlr',
-              'csms',
-              'idtag',
-              'idtags',
-              'iccid',
-              'imsi',
-              'ocpp',
-              'onconnection',
-              'opencode',
-              'evse',
-              'evses',
-              'kvar',
-              'kvarh',
-              'varh',
-              'rfid',
-              'workerset',
-              'worktree',
-              'dedup',
-              'unpushed',
-              'logform',
-              'mnemonist',
-              'poolifier',
-              'measurand',
-              'measurands',
-              'mikro',
-              'neostandard',
-              'recurrency',
-              'shutdowning',
-              'VCAP',
-              'workerd',
-              'yxxx',
-              // OCPP 2.0.x domain terms
-              'cppwm',
-              'heartbeatinterval',
-              'HEARTBEATINTERVAL',
-              'websocketpinginterval',
-              'WEBSOCKETPINGINTERVAL',
-              'connectionurl',
-              'CONNECTIONURL',
-              'chargingstation',
-              'CHARGINGSTATION',
-              'authctrlr',
-              'AUTHCTRLR',
-              'recloser',
-              'deauthorize',
-              'Deauth',
-              'DEAUTHORIZE',
-              'deauthorized',
-              'DEAUTHORIZED',
-              'Deauthorization',
-              'Selftest',
-              'SECC',
-              'Secc',
-              'Overcurrent',
-              'ocsp',
-              'OCSP',
-              'EMAID',
-              'emaid',
-              'IDTOKEN',
-              'idtoken',
-              'issuerkeyhash',
-              'issuernamehash',
-              // OCPP SRPC
-              'SRPC',
-              'CALLRESULT',
-              'CALLERROR',
-              'CALLRESULTERROR',
-              'reservability',
-              // VPN protocol acronyms
-              'PPTP',
-              // UI server protocol acronyms
-              'UIMCP',
-              'Streamable',
-              'modelcontextprotocol',
-              // Signed meter values
-              'OCMF',
-              'ocmf',
-              'secp',
-              'brainpool',
-              'Eichrecht',
-              'eichrecht',
-              'signingmethod',
-              'SIGNINGMETHOD',
-              'encodingmethod',
-              'ENCODINGMETHOD',
-              'signedmeterdata',
-              'SIGNEDMETERDATA',
-              'fiscalmetering',
-              'FISCALMETERING',
-              'publickeywithsignedmetervalue',
-              'PUBLICKEYWITHSIGNEDMETERVALUE',
-              'sampleddatasignreadings',
-              'SAMPLEDDATASIGNREADINGS',
-              // UI component terms
-              'focusables',
-              'Focusables',
-              // Test credential fragments
-              'secret',
-            ],
-          },
-        },
-      ],
+      '@cspell/spellchecker': ['warn', { autoFix: true }],
     },
   },
   js.configs.recommended,
@@ -146,27 +40,27 @@ export default defineConfig([
       ],
     },
   },
+
+  // neostandard
+
+  ...neostandard({ ts: true }),
+
+  // Vue
+
   ...pluginVue.configs['flat/recommended'],
-  {
-    files: ['**/*.vue'],
-    languageOptions: {
-      globals: {
-        localStorage: 'readonly',
-      },
-      parserOptions: {
-        parser: '@typescript-eslint/parser',
-      },
-    },
-  },
+
+  // TypeScript
+
   ...plugins['typescript-eslint'].config(
     {
       extends: [
         ...plugins['typescript-eslint'].configs.strictTypeChecked,
         ...plugins['typescript-eslint'].configs.stylisticTypeChecked,
       ],
-      files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '*/**.vue'],
+      files: GLOB_TS_VUE,
       languageOptions: {
         parserOptions: {
+          extraFileExtensions: ['.vue'],
           projectService: true,
           // eslint-disable-next-line n/no-unsupported-features/node-builtins
           tsconfigRootDir: import.meta.dirname,
@@ -186,10 +80,32 @@ export default defineConfig([
       },
     },
     {
-      files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
+      files: GLOB_JS,
       ...plugins['typescript-eslint'].configs.disableTypeChecked,
     }
   ),
+
+  // Vue parser restoration
+
+  {
+    files: ['**/*.vue'],
+    languageOptions: {
+      globals: {
+        localStorage: 'readonly',
+      },
+      parser: vueParser,
+      parserOptions: {
+        extraFileExtensions: ['.vue'],
+        parser: plugins['typescript-eslint'].parser,
+        projectService: true,
+        // eslint-disable-next-line n/no-unsupported-features/node-builtins
+        tsconfigRootDir: import.meta.dirname,
+      },
+    },
+  },
+
+  // Perfectionist
+
   perfectionist.configs['recommended-natural'],
   {
     files: ['**/*.vue'],
@@ -197,11 +113,11 @@ export default defineConfig([
       'perfectionist/sort-vue-attributes': 'off',
     },
   },
-  ...neostandard({
-    ts: true,
-  }),
+
+  // Rule overrides
+
   {
-    files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.vue'],
+    files: GLOB_TS_VUE,
     rules: {
       '@typescript-eslint/no-unused-vars': [
         'error',
index 8b7476f41a5c74aeb72b298094622a68bc10b1df..2a5c0e23e078fc31ff0581a3789088fbb2ec4cb2 100644 (file)
@@ -21,7 +21,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, defineAsyncComponent, markRaw, watch } from 'vue'
+import { type Component, computed, defineAsyncComponent, markRaw, watch } from 'vue'
 
 import { ASYNC_COMPONENT_DELAY_MS, ASYNC_COMPONENT_TIMEOUT_MS } from '@/core/index.js'
 import SkinLoadError from '@/shared/components/SkinLoadError.vue'
@@ -43,9 +43,9 @@ const skinLayoutMap = new Map(
     markRaw(
       defineAsyncComponent({
         delay: ASYNC_COMPONENT_DELAY_MS,
-        errorComponent: SkinLoadError,
+        errorComponent: SkinLoadError as Component,
         loader: () => s.loadLayout(),
-        loadingComponent: SkinLoading,
+        loadingComponent: SkinLoading as Component,
         timeout: ASYNC_COMPONENT_TIMEOUT_MS,
       })
     ),
index 395d4114fbbf57a0ad8010cc063ec67e96f918c9..e325bb13ed100b9b39a0c3e5f2fd04898051fa82 100644 (file)
           :key="state.renderAddChargingStations"
           :off="
             () => {
-              $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+              $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
             }
           "
           :on="
             () => {
-              $router.push({ name: ROUTE_NAMES.ADD_CHARGING_STATIONS })
+              $router.push({ name: ROUTE_NAMES.ADD_CHARGING_STATIONS }).catch(() => undefined)
             }
           "
           :shared="true"
@@ -166,7 +166,7 @@ const {
     clearToggleButtons()
     refresh()
     if ($route.name !== ROUTE_NAMES.CHARGING_STATIONS) {
-      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
     }
   },
   onSimulatorStopped: () => {
index 26b05c8c5d86df3189b975de2ebcc84f97eea09d..87dc0a9881a31bc48f780e6690d6ba593aae46e5 100644 (file)
@@ -133,7 +133,7 @@ const $router = useRouter()
 const { formState, pending, submitForm, templates } = useAddStationsForm({
   onFinally: () => {
     resetToggleButtonState('add-charging-stations', true)
-    $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+    $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
   },
 })
 
index 512d3fc0963d2ea6bc6c0d2de6db89ba29faf7f1..af86c28282039edb1ec4941ff1549eceb0e8266e 100644 (file)
@@ -61,7 +61,7 @@ const setSupervisionUrl = async (): Promise<void> => {
   const success = await submitForm()
   if (success) {
     resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
-    $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+    $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
   }
 }
 </script>
index 4c51be8fedfa8903f566ecbe31ef8a4459bd3631..6b5720a9127a9fb94e287595fceceb4b9af49c11 100644 (file)
@@ -82,7 +82,7 @@ const { formState, submitForm } = useStartTxForm({
 
 const handleStartTransaction = async (): Promise<void> => {
   await submitForm()
-  $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+  $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
 }
 </script>
 
index 123b11d75d0270ddbe35c1c0ae5022c7663c8152..de57002e33e07b50913241b457254837df478a61 100644 (file)
@@ -30,17 +30,16 @@ const props = defineProps<{
 
 const emit = defineEmits<{ clicked: [status: boolean] }>()
 
-const id =
-  props.shared === true
-    ? `${SHARED_TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
-    : `${TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
+const id = props.shared
+  ? `${SHARED_TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
+  : `${TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
 
 const state = ref<{ status: boolean }>({
-  status: getFromLocalStorage<boolean>(id, props.status ?? false),
+  status: getFromLocalStorage<boolean>(id, props.status || false),
 })
 
 const click = (): void => {
-  if (props.shared === true) {
+  if (props.shared) {
     try {
       const keys = Object.keys(getLocalStorage()).filter(
         key => key !== id && key.startsWith(SHARED_TOGGLE_BUTTON_KEY_PREFIX)
@@ -54,7 +53,7 @@ const click = (): void => {
       }
     }
   }
-  const current = getFromLocalStorage<boolean>(id, props.status ?? false)
+  const current = getFromLocalStorage<boolean>(id, props.status || false)
   const newStatus = !current
   setToLocalStorage<boolean>(id, newStatus)
   state.value.status = newStatus
index 4c4e292ff259d735a037ba748f011d87e034aadf..e2779eaca8053d99646c13cf831f8533694b9673 100644 (file)
@@ -54,7 +54,7 @@
         :id="`${hashId}-${evseId ?? 0}-${connectorId}-start-transaction`"
         :off="
           () => {
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
           }
         "
         :on="
@@ -66,7 +66,7 @@
                 ...(evseId != null ? { evseId: String(evseId) } : {}),
                 ...(ocppVersion != null ? { ocppVersion } : {}),
               },
-            })
+            }).catch(() => undefined)
           }
         "
         :shared="true"
index 4c1bd2ff3750b77845fc093e706681b89fc3d561..a2dbfa000a87c915009138d3da2c871811bd7bf1 100644 (file)
@@ -49,7 +49,7 @@
         :id="`${chargingStation.stationInfo.hashId}-set-supervision-url`"
         :off="
           () => {
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
           }
         "
         :on="
@@ -60,7 +60,7 @@
                 hashId: chargingStation.stationInfo.hashId,
                 chargingStationId: chargingStation.stationInfo.chargingStationId,
               },
-            })
+            }).catch(() => undefined)
           }
         "
         :shared="true"
@@ -159,7 +159,11 @@ const {
   openConnection,
   startStation: startChargingStation,
   stopStation: stopChargingStation,
-} = useStationActions({ onRefresh: () => emit('need-refresh') })
+} = useStationActions({
+  onRefresh: () => {
+    emit('need-refresh')
+  },
+})
 
 const hashId = computed(() => props.chargingStation.stationInfo.hashId)
 
index a3a501d2e7422159b96d1ef31c41e61b570311b3..5d2c300b773c9cd764b50d7896c1bb3f8755a2d9 100644 (file)
@@ -75,9 +75,8 @@
 </template>
 
 <script setup lang="ts">
-// Dialog state via v-if (no URL coupling), enabling skin-independent modal interactions.
 import { type OCPPVersion } from 'ui-common'
-import { defineAsyncComponent, ref, watch } from 'vue'
+import { type Component, defineAsyncComponent, ref, watch } from 'vue'
 
 import {
   ASYNC_COMPONENT_DELAY_MS,
@@ -95,17 +94,25 @@ import ConfirmDialog from './components/ConfirmDialog.vue'
 import SimulatorBar from './components/SimulatorBar.vue'
 import StationCard from './components/StationCard.vue'
 
+interface StartTxDialogPayload {
+  chargingStationId: string
+  connectorId: string
+  evseId?: number
+  hashId: string
+  ocppVersion?: OCPPVersion
+}
+
 /**
  * Creates a lazy-loaded dialog component with shared loading/error boundaries.
  * @param loader - Dynamic import function for the dialog component
  * @returns An async component definition with standardized loading and error states
  */
-function defineAsyncDialog (loader: () => Promise<{ default: unknown }>) {
+function defineAsyncDialog (loader: () => Promise<{ default: Component }>) {
   return defineAsyncComponent({
     delay: ASYNC_COMPONENT_DELAY_MS,
-    errorComponent: SkinLoadError,
-    loader: loader as () => Promise<{ default: import('vue').Component }>,
-    loadingComponent: SkinLoading,
+    errorComponent: SkinLoadError as Component,
+    loader,
+    loadingComponent: SkinLoading as Component,
     timeout: ASYNC_COMPONENT_TIMEOUT_MS,
   })
 }
@@ -152,13 +159,7 @@ const showSetUrlDialog = ref<null | {
   chargingStationId: string
   hashId: string
 }>(null)
-const showStartTxDialog = ref<null | {
-  chargingStationId: string
-  connectorId: string
-  evseId?: number
-  hashId: string
-  ocppVersion?: OCPPVersion
-}>(null)
+const showStartTxDialog = ref<null | StartTxDialogPayload>(null)
 const showAuthorizeDialog = ref<null | {
   chargingStationId: string
   hashId: string
index 8c744b67b32bc17d8e90a4f6eef306e2704e82b2..a10163e319f42e7d2418f05b57fae292b0ff2a20 100644 (file)
@@ -234,7 +234,9 @@ const {
 })
 
 const identifier = computed(() =>
-  props.evseId != null ? `${props.evseId}/${props.connectorId}` : String(props.connectorId)
+  props.evseId != null
+    ? `${String(props.evseId)}/${String(props.connectorId)}`
+    : String(props.connectorId)
 )
 
 const showSetConnectorStatus = ref(false)
@@ -264,7 +266,7 @@ const txEnergy = computed(() => {
   const wh = props.connector.transactionEnergyActiveImportRegisterValue
   if (wh == null) return '—'
   if (wh >= WH_PER_KWH) return `${(wh / WH_PER_KWH).toFixed(2)} kWh`
-  return `${Math.round(wh)} Wh`
+  return `${String(Math.round(wh))} Wh`
 })
 
 const toggleLock = (): void => {
index c251a948e19b7174b5cbac74d71e74277eca9745..c4a4f9d46cb3dc060873392310dda5e329c9f0ce 100644 (file)
@@ -89,13 +89,12 @@ const collectFocusables = (): HTMLElement[] => {
 const focusFirst = (): void => {
   if (dialogEl.value == null) return
   const focusables = collectFocusables()
-  // Prefer first non-close button so the user lands on a real input.
-  const target = focusables.find(el => !el.hasAttribute('data-modal-close')) ?? focusables[0]
-  if (target != null) {
-    target.focus()
-  } else {
+  if (focusables.length === 0) {
     dialogEl.value.focus()
+    return
   }
+  const target = focusables.find(el => !el.hasAttribute('data-modal-close')) ?? focusables[0]
+  target.focus()
 }
 
 const handleEsc = (): void => {
@@ -147,7 +146,7 @@ onMounted(() => {
 
 onBeforeUnmount(() => {
   document.body.style.overflow = ''
-  previouslyFocused?.focus?.()
+  previouslyFocused?.focus()
 })
 </script>
 
index 7f18761440e1777043c9289ba7fa366f8c224c72..158a88aacbb4f989f86f05893dc40e81565205da 100644 (file)
@@ -101,7 +101,6 @@ import StatePill from './StatePill.vue'
  * @returns The selected option's index
  */
 function getSelectIndex (e: Event): number {
-  // eslint-disable-next-line no-undef
   return (e.target as HTMLSelectElement).selectedIndex
 }
 
@@ -131,7 +130,7 @@ const simulatorVariant = computed<'err' | 'idle' | 'ok'>(() => {
 
 const simulatorLabel = computed(() => {
   if (props.simulatorState == null) return 'Disconnected'
-  const version = props.simulatorState.version != null ? ` (${props.simulatorState.version})` : ''
+  const version = props.simulatorState.version ? ` (${props.simulatorState.version})` : ''
   return `${simulatorStarted.value ? 'Running' : 'Stopped'}${version}`
 })
 </script>
index 3fd17d98d68b1cecff37db3a6ed1d2d78ae75b5c..88c37bb2df9f83620c80d6a900fe0756d9fd2d18 100644 (file)
@@ -206,9 +206,7 @@ const { closeConnection, deleteStation, openConnection, pending, startStation, s
 
 const wsOpen = computed(() => props.chargingStation.wsState === WebSocketReadyState.OPEN)
 
-const startedVariant = computed<'err' | 'ok'>(() =>
-  props.chargingStation.started === true ? 'ok' : 'err'
-)
+const startedVariant = computed<'err' | 'ok'>(() => (props.chargingStation.started ? 'ok' : 'err'))
 
 const wsVariant = computed(() => getWebSocketStateVariant(props.chargingStation.wsState))
 
@@ -226,7 +224,7 @@ const getATGStatusForConnector = (connectorId: number): Status | undefined =>
 
 const toggleStation = (): void => {
   const hashId = props.chargingStation.stationInfo.hashId
-  if (props.chargingStation.started === true) {
+  if (props.chargingStation.started) {
     stopStation(hashId)
   } else {
     startStation(hashId)
index d57764d92760007ae7ff7150947f0c023962bb91..e173305fc49518a0593dd47eb359c69990f16406 100644 (file)
@@ -112,8 +112,8 @@ watch(
   station => {
     if (station != null) {
       formState.value.supervisionUrl = stripStationId(
-        station.supervisionUrl ?? '',
-        station.stationInfo.chargingStationId ?? ''
+        station.supervisionUrl,
+        station.stationInfo.chargingStationId
       )
       formState.value.supervisionUser = station.stationInfo.supervisionUser ?? ''
       formState.value.supervisionPassword = station.stationInfo.supervisionPassword ?? ''