Type: bug
Description
Under Domains → <domain> → SYNC POLICY, the three text fields cannot be saved. Pressing
SAVE returns Bad request. Checkboxes and sliders on the same form save without problems.
Affected fields:
| Label in the UI |
Key |
| Password expiration (days) |
devpwexpiration |
| Inactivity (seconds) before device locks itself |
maxinacttimedevlock |
| Password history |
devpwhistory |
The API rejects the request:
PATCH /api/v1/system/domains/1? -> 400
{"errors":["Value {'domainname': 'example.com', 'domainStatus': 0, 'orgID': 0, 'maxUser': 100,
..., 'syncPolicy': {'devpwenabled': 1, 'devpwhistory': '0'}, 'chat': False} not valid for
schema of type SchemaType.ANY: (<ValidationError: \"'0' is not of type integer\">,)"],
"message":"Bad Request"}
Note 'devpwhistory': '0' — a string. The API schema is right to refuse it;
res/config.yaml declares devpwhistory, devpwexpiration and maxinacttimedevlock as
type: integer, default: 0.
Steps to reproduce
Domains → <any domain> → SYNC POLICY
- Change Password history (any value, including typing back the default
0)
- SAVE → Bad request
Cause
ChangeEvent.target.value is always a string. The checkbox and slider handlers coerce to
numbers, the text handler does not.
containers/DomainDetails.tsx:
230: const handleSyncChange = (field: string) => (event: ChangeEvent) => {
231: const { syncPolicy } = state;
232: setState({
233: ...state,
234: syncPolicy: {
235: ...syncPolicy,
236: [field]: event.target.value, // <-- string
237: },
compared with the two handlers next to it, which produce numbers:
handleSyncCheckboxChange: [field]: newValue ? 1 : 0
handleSlider: [field]: newVal
utils.tsx then normalises the policy before diffing, but || 0 only replaces
undefined/null/"". A non-empty string passes straight through, and "0" is truthy:
199: export function getPolicyDiff(defaultPolicy: Partial<SyncPolicy>, syncPolicy: Partial<SyncPolicy>): Partial<SyncPolicy> {
200: const formattedPolicy: Partial<SyncPolicy> = {
201: ...syncPolicy,
202: devpwhistory: syncPolicy.devpwhistory || 0,
203: devpwexpiration: syncPolicy.devpwexpiration || 0,
204: maxinacttimedevlock: syncPolicy.maxinacttimedevlock || 0,
205: };
206: const result: Record<string, any> = {};
207: for(const [key, value] of Object.entries(defaultPolicy)) {
208: if (formattedPolicy[key as keyof SyncPolicy] !== value) {
209: result[key] = formattedPolicy[key as keyof SyncPolicy];
Line 208 is worth a second look: the comparison is strict, so "0" !== 0 is true. Typing the
default value back into the field is enough to put it in the diff — and therefore enough to fail
the save. The user sees a rejection for a change that amounts to nothing.
components/SyncPolicies.tsx:73 wires the text fields to that handler, so all three share the
fault:
69: value: syncPolicy[field],
...
73: onChange: handleChange(field),
The same handler exists at user level with the same defect —
containers/UserDetails.tsx:621, [field]: event.target.value — so
Users → <user> → SYNC POLICY is affected identically.
Suggested fix
Coerce where the value enters state, in both containers:
const handleSyncChange = (field: string) => (event: ChangeEvent) => {
const { syncPolicy } = state;
setState({
...state,
syncPolicy: {
...syncPolicy,
- [field]: event.target.value,
+ [field]: event.target.value === "" ? "" : Number(event.target.value),
},
});
Keeping "" as-is lets the field be cleared while typing; getPolicyDiff already maps it to 0.
Alternatively, or in addition, make the normalisation in utils.tsx do the conversion it looks
like it is doing:
- devpwhistory: syncPolicy.devpwhistory || 0,
- devpwexpiration: syncPolicy.devpwexpiration || 0,
- maxinacttimedevlock: syncPolicy.maxinacttimedevlock || 0,
+ devpwhistory: Number(syncPolicy.devpwhistory) || 0,
+ devpwexpiration: Number(syncPolicy.devpwexpiration) || 0,
+ maxinacttimedevlock: Number(syncPolicy.maxinacttimedevlock) || 0,
That second hunk alone fixes the reported failure and also removes the spurious diff entry on
line 208. Doing both keeps the state itself correctly typed, which matters because
SyncPolicy declares these fields as numbers — TypeScript does not catch the mismatch today
because ChangeEvent is untyped at that point.
Workaround
grommunio-admin domain modify has no syncPolicy option, so the only way to set one of the
three fields is to PATCH the API directly with a correctly typed value.
Not a duplicate of #11
#11 (closed, fixed April 2026) is the same
class of defect — the web client sending values the API schema refuses — but a different
instance: it was None for non-nullable fields when creating a domain. This one is a string
where an integer is required when updating one, and it originates in handleSyncChange /
getPolicyDiff, which #11 did not touch. The failure is still reproducible on the versions below.
All 18 issues and pull requests in grommunio/admin-web were fetched and searched locally for
syncPolicy, sync policy, devpw, integer, maxinact and policy, open and closed alike.
#11 is the only hit, and only on the words "bad request".
Versions
grommunio-admin-web 5.0.0.62.0ad249e-1+203.1
grommunio-admin-api 1.20.43.mbb64403-1+64.1
Ubuntu 24.04, community repo. Source read from the shipped source maps in
/usr/share/grommunio-admin-web/static/js/*.map, so line numbers are from the released build.
Type: bug
Description
Under
Domains → <domain> → SYNC POLICY, the three text fields cannot be saved. PressingSAVE returns Bad request. Checkboxes and sliders on the same form save without problems.
Affected fields:
devpwexpirationmaxinacttimedevlockdevpwhistoryThe API rejects the request:
Note
'devpwhistory': '0'— a string. The API schema is right to refuse it;res/config.yamldeclaresdevpwhistory,devpwexpirationandmaxinacttimedevlockastype: integer, default: 0.Steps to reproduce
Domains → <any domain> → SYNC POLICY0)Cause
ChangeEvent.target.valueis always a string. The checkbox and slider handlers coerce tonumbers, the text handler does not.
containers/DomainDetails.tsx:compared with the two handlers next to it, which produce numbers:
utils.tsxthen normalises the policy before diffing, but|| 0only replacesundefined/null/"". A non-empty string passes straight through, and"0"is truthy:Line 208 is worth a second look: the comparison is strict, so
"0" !== 0istrue. Typing thedefault value back into the field is enough to put it in the diff — and therefore enough to fail
the save. The user sees a rejection for a change that amounts to nothing.
components/SyncPolicies.tsx:73wires the text fields to that handler, so all three share thefault:
The same handler exists at user level with the same defect —
containers/UserDetails.tsx:621,[field]: event.target.value— soUsers → <user> → SYNC POLICYis affected identically.Suggested fix
Coerce where the value enters state, in both containers:
const handleSyncChange = (field: string) => (event: ChangeEvent) => { const { syncPolicy } = state; setState({ ...state, syncPolicy: { ...syncPolicy, - [field]: event.target.value, + [field]: event.target.value === "" ? "" : Number(event.target.value), }, });Keeping
""as-is lets the field be cleared while typing;getPolicyDiffalready maps it to0.Alternatively, or in addition, make the normalisation in
utils.tsxdo the conversion it lookslike it is doing:
That second hunk alone fixes the reported failure and also removes the spurious diff entry on
line 208. Doing both keeps the state itself correctly typed, which matters because
SyncPolicydeclares these fields as numbers — TypeScript does not catch the mismatch todaybecause
ChangeEventis untyped at that point.Workaround
grommunio-admin domain modifyhas nosyncPolicyoption, so the only way to set one of thethree fields is to PATCH the API directly with a correctly typed value.
Not a duplicate of #11
#11 (closed, fixed April 2026) is the same
class of defect — the web client sending values the API schema refuses — but a different
instance: it was
Nonefor non-nullable fields when creating a domain. This one is a stringwhere an integer is required when updating one, and it originates in
handleSyncChange/getPolicyDiff, which #11 did not touch. The failure is still reproducible on the versions below.All 18 issues and pull requests in
grommunio/admin-webwere fetched and searched locally forsyncPolicy,sync policy,devpw,integer,maxinactandpolicy, open and closed alike.#11 is the only hit, and only on the words "bad request".
Versions
Ubuntu 24.04, community repo. Source read from the shipped source maps in
/usr/share/grommunio-admin-web/static/js/*.map, so line numbers are from the released build.