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.
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).
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).
- 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.
- 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).