]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(webui): allow editing charging station configuration (#2077)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 13 Aug 2026 19:37:03 +0000 (21:37 +0200)
committerGitHub <noreply@github.com>
Thu, 13 Aug 2026 19:37:03 +0000 (21:37 +0200)
commitaa432cb88d85abf304b662691df416e1c288ef82
tree30ae5b730be34c201a61634f914ad3394bede90f
parent4fb658ce70d18d728e58b02dcb8e22451aedf163
feat(webui): allow editing charging station configuration (#2077)

* feat(webui): allow editing charging station configuration

Add a generic, data-driven editor for a charging station's current OCPP
configurationKey values in the Web UI (both classic and modern skins),
closing #1828.

A new CHANGE_CONFIGURATION UI protocol verb clones the SET_SUPERVISION_URL
chain end-to-end (UIClient -> ProcedureName/BroadcastChannelProcedureName ->
AbstractUIService mapping -> worker handler). To stay OCPP spec-faithful, the
change is applied through a new version-agnostic
ChargingStation.changeConfiguration seam that reuses the existing OCPP 1.6
ChangeConfiguration and OCPP 2.0.1 SetVariables handler logic (readonly
rejection, integer/bounds validation, heartbeat/WS-ping restarts, reboot
signalling) without emitting a CSMS response, then emits
ChargingStationEvents.updated so the UI refreshes.

OCPP 2.0.1 configurationKey entries are now seeded with their registry
mutability/rebootRequired flags, so readonly keys are correctly disabled in
the UI and reboot keys are flagged.

Refs: #1828

* docs(webui): mention edit configuration action

* test(webui): cover changeConfiguration emit gating, spec-fidelity and OCPP 2.0.1 edge cases

Address PR review coverage gaps:
- assert ChargingStation.changeConfiguration emits ChargingStationEvents.updated
  only on Accepted/RebootRequired (drives the read-view refresh)
- assert the OCPP 1.6 seam applies changes without emitting a CSMS response
- add OCPP 2.0.1 seam read-only rejection and reboot-required cases

* refactor(webui): address review findings for change-configuration

- OCPP 2.0.1 seam: set the resolved `instance` only on the SetVariables
  `component` (drop the redundant `variable.instance`); the registry models
  the instance as component-scoped and internal resolution reads
  `variable.instance ?? component.instance`, so this is behavior-neutral.
  Add an instance-scoped non-regression test (TariffCostCtrlr.Enabled.Cost).
- useStationDetails: docstring no longer claims "read-only" now that it
  exposes editableConfigurationKeys.
- ui/web README: "edit configuration" -> "change configuration" to match the
  UI/code terminology.
- ModernLayout: order the ChangeConfigurationDialog async declaration
  alphabetically and put :hash-id before :charging-station-id, matching the
  sibling dialogs.

* refactor(webui): hoist change-configuration draft state into the shared composable

Address the second-round review findings:
- M1 (DRY): move the per-key draft values, seeding watch and save() from both
  skin components into useChangeConfigurationForm (now takes the editable keys
  ref and exposes { draftValues, pending, save }), mirroring useSetUrlForm which
  owns its form state. The classic action and modern dialog become pure UI.
- M2: add direct unit tests for the worker CHANGE_CONFIGURATION handler
  (delegation, empty-value accepted, missing/empty/non-string key or value
  rejected with BaseError).
- M3: align residual "edit"/"editing" wording with the "change configuration"
  terminology in docstrings (kept editableConfigurationKeys as a property name).

* refactor(webui): validate required broadcast-channel string fields with isNotEmptyString

Replace the raw `typeof x !== 'string' || isEmpty(x)` guards in the
CHANGE_CONFIGURATION and SET_SUPERVISION_URL worker handlers with the
`!isNotEmptyString(x)` type guard (the repo-wide idiom, ~78 usages), which also
narrows the field to a non-empty string. Both handlers are switched together so
the file keeps a single convention. `isEmpty` remains used for the
empty-response status checks; the `value` field stays `typeof value !== 'string'`
since an empty value is a legitimate configuration value.

* refactor(webui): tidy change-configuration naming, reverse-map and MCP schema

Third-round review nits:
- M1: rename `editableConfigurationKeys` -> `visibleConfigurationKeys` (the
  collection includes read-only keys; the name now matches its source util
  getVisibleConfigurationKeys). Propagated across useStationDetails,
  useChangeConfigurationForm, both skin components and the tests.
- M2: build OCPP20VariableManager's flat-key reverse map as a declarative
  private instance field (like #validComponentNames) instead of a mutable
  module-level for-loop; the dedup guard was inert (composite key names are
  unique).
- T1: inline the single-caller private `submit` into `save`.
- T2: enforce a non-empty `key` at the MCP option layer (z.string().min(1));
  `value` stays unconstrained (empty is a valid configuration value).

* test(webui): cover change-configuration components

Add component tests for the classic `ChangeConfiguration.vue` action and the
modern `ChangeConfigurationDialog.vue`, which were shipped without component
tests and dropped ui/web coverage below the CI thresholds (the failing
`Build dashboard / Node 24.x / ubuntu-latest` cell runs `pnpm test:coverage`).

Tests exercise the observable contracts already verified in-browser: not-found
panel, empty-state, non-visible key exclusion, read-only input/button disabling,
editable save invoking `changeConfiguration`, reboot-required notice, read-only
save guard and backend-rejection error toast. Adds the missing
`changeConfiguration` method to the shared `MockUIClient`.

Refs: #1828

* fix(ocpp): reject empty value for integer 1.6 configuration keys

Address the initial-review findings on PR #2077:

- T1 (fix): the OCPP 1.6 ChangeConfiguration handler accepted an empty/blank
  value for integer keys (Number('') === 0 passed the non-negative-integer
  check) and persisted the raw empty string. Reject it explicitly via
  isNotEmptyString, matching the spec (values not conforming to the expected
  integer format are Rejected). Covers both the UI seam and the OCPP wire path.
  Add a discriminating test (empty value -> REJECTED, value preserved).
- T2 (fix): correct the misleading worker error message
  "'value' field is required" -> "'value' field must be a string" (an empty
  string is a legitimate value; only a non-string is rejected).
- T3 (test): rename the composable test description "editable keys" ->
  "visible keys" to match the renamed visibleConfigurationKeys parameter.
- M1 (docs): document, on OCPP20 changeConfiguration, that the resolved
  instance is carried on component.instance and is internal-only (never
  emitted to a CSMS), so the component- vs variable-instance distinction is
  immaterial here.
- M2 (docs): add the missing 'changeConfiguration' ProcedureName block to the
  README UI protocol reference.

Kept as-is with rationale: the version-agnostic seam reuses the 1.6-named
ChangeConfigurationResponse type (established CommandResponse union convention,
type-safe alias); the modern dialog table style follows the skin's
per-component scoped convention. Cleartext config values in the edit UI are
pre-existing (already exposed via LIST_CHARGING_STATIONS / MCP) and out of
scope.

Refs: #1828

* docs: clarify changeConfiguration value validation in the README protocol reference

Address review finding TR1: the changeConfiguration block claimed "an empty
string is allowed", which is only true at the message-envelope level. A value
invalid for the target key (e.g. a non-integer or empty value for a numeric
1.6 key) is rejected. Reword the value clause to state that invalid values are
rejected and keep it version-agnostic (the verb serves both OCPP 1.6 and
2.0.1). No code change.

Refs: #1828

* test(webui): factor duplicated configuration-keys station fixture into a shared factory

The `stationWithKeys` helper was duplicated verbatim in the classic and modern
skin test files. Mirroring the test-factorization convention consolidated in
main (PR #2078: shared test helpers, no per-file scaffolding duplication),
extract it into the canonical fixture home `tests/unit/constants.ts` as
`createStationWithConfigurationKeys`, and migrate both skin test suites to it.
Removes the now-unused local helpers and `ConfigurationKey` imports. No behavior
change (ui/web 592/592).

Refs: #1828

* test(webui): migrate remaining inline config-key fixtures to the shared factory

Address review finding TR1: complete the DRY migration started with
createStationWithConfigurationKeys. Replace the 13 remaining inline
`createChargingStationData({ ocppConfiguration: { configurationKey: … } })`
sole-override fixtures across the stationDetails, useStationDetails, ShowDetails
and ShowDetailsDialog test blocks with the shared factory. The empty-
ocppConfiguration case (stationDetails.test.ts, tests the absent-key fallback)
and all sites carrying additional overrides are intentionally left inline. No
behavior change (ui/web 592/592).

Refs: #1828

* docs(ocpp): tighten change-configuration rationale comments

- OCPP16 integer-key guard: consolidate the two lines commenting the same guard
  into one coherent, accurate rationale — why Number() over convertToInt
  (truncates '1.5' → 1, throws on ''/'abc') and why the explicit isNotEmptyString
  check. Number() yields a non-integer float ('1.5' → 1.5) or NaN ('abc'), both
  caught by !Number.isInteger; isNotEmptyString rejects '' (Number('') === 0 would
  otherwise pass).
- OCPP20 changeConfiguration JSDoc: remove redundancy with the delegate's JSDoc
  and compress the instance-placement caveat, keeping every non-derivable fact.

Comment-only change; no behavior change.

Refs: #1828

* refactor(webui): single-source config-key formatting and align a11y state

- Export the shared formatBoolean helper and reuse it for the Readonly/Reboot
  cells in both change-configuration skins (drop the 4 inline Yes/No literals).
- Classic Save button: expose aria-busy while a change is in flight, matching
  the modern ActionButton (no visual spinner: classic has no such token).
- Drop the redundant aria-labelledby (and its useId) on the modern table whose
  <caption> already names it, and the partial aria-disabled on the inputs whose
  native disabled is authoritative.
- Add tests: Readonly/Reboot cell rendering and in-flight aria-busy in both
  skins, plus modern parity for reboot-notice, read-only-not-submitted and
  error-toast (mirroring the classic suite).

Refs: #1828

* docs: clarify metadata-driven reboot notice and safe 2.0.1 status collapse

- useChangeConfigurationForm: note that the reboot notice derives from the key
  metadata, not the runtime status, because the worker response collapses
  ACCEPTED|REBOOT_REQUIRED into a boolean success.
- OCPP20 status mapping: note that UnknownComponent/UnknownVariable are
  unreachable via changeConfiguration (resolveConfigurationKeyName gates unknown
  keys) and that Record exhaustiveness is compile-enforced.

Refs: #1828

* [autofix.ci] apply automated fixes

* test(webui): harden change-configuration table tests

- Yes/No cell test: use column-asymmetric keys (readonly-only vs reboot-only)
  so an accidental swap of the Readonly/Reboot columns fails the assertion.
- Add a guard that the OCPP parameters table is named via its <caption> in
  both skins (the modern table dropped its redundant aria-labelledby).

Refs: #1828

* refactor(ocpp): use isEmpty for the SetVariables result check

Replace `response.setVariableResult.length === 0` with `isEmpty(...)` in the
2.0.1 changeConfiguration seam, matching the repo convention (isEmpty is
already imported and used for arrays in this file).

Refs: #1828

* test(webui): fit CHANGE_CONFIGURATION worker tests into the group structure

The two CHANGE_CONFIGURATION describes (status collapse + handler) were wedged
between Group 1 and Group 2 without a group banner. Relocate them after Group 2
under a numbered "Group 3" banner (restoring ascending group order and filling
the pre-existing gap), and correct the stale Group 4 count (8 -> 9 tests).

Refs: #1828

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
36 files changed:
README.md
src/charging-station/ChargingStation.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20VariableManager.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ui-server/mcp/MCPToolSchemas.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/types/UIProtocol.ts
src/types/WorkerBroadcastChannel.ts
tests/charging-station/ChargingStation-ChangeConfiguration.test.ts [new file with mode: 0644]
tests/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.test.ts
tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-Configuration.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeConfiguration.test.ts [new file with mode: 0644]
ui/common/src/types/UIProtocol.ts
ui/web/README.md
ui/web/src/core/Constants.ts
ui/web/src/core/UIClient.ts
ui/web/src/router/index.ts
ui/web/src/shared/composables/useChangeConfigurationForm.ts [new file with mode: 0644]
ui/web/src/shared/composables/useStationDetails.ts
ui/web/src/shared/utils/index.ts
ui/web/src/shared/utils/stationDetails.ts
ui/web/src/skins/classic/components/actions/ChangeConfiguration.vue [new file with mode: 0644]
ui/web/src/skins/classic/components/charging-stations/CSData.vue
ui/web/src/skins/modern/ModernLayout.vue
ui/web/src/skins/modern/components/StationCard.vue
ui/web/src/skins/modern/components/dialogs/ChangeConfigurationDialog.vue [new file with mode: 0644]
ui/web/tests/unit/constants.ts
ui/web/tests/unit/helpers.ts
ui/web/tests/unit/shared/composables/stationDetails.test.ts
ui/web/tests/unit/shared/composables/useChangeConfigurationForm.test.ts [new file with mode: 0644]
ui/web/tests/unit/shared/composables/useStationDetails.test.ts
ui/web/tests/unit/skins/classic/Actions.test.ts
ui/web/tests/unit/skins/modern/Dialogs.test.ts