Skip to content

sync: settings/configuration and page fixes (#759, #760, #762, #813), plus merge and CI guards - #818

Merged
ako merged 25 commits into
mendixlabs:mainfrom
ako:main
Aug 1, 2026
Merged

sync: settings/configuration and page fixes (#759, #760, #762, #813), plus merge and CI guards#818
ako merged 25 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Sync of ako/mxcli:main into upstream. Four reported issues fixed, each verified
against a real Mendix project rather than by inspection, plus infrastructure fixes
found along the way.

Settings and configuration

  • Runtime Configuration ( Create or Update ) are not fully supported #759 — runtime configuration create/update writes properties Mendix cannot resolve.
    Studio Pro threw System.InvalidOperationException: Sequence contains no matching element at MprProperty.cs:25 while mx check passed (mxbuild tolerates unknown
    properties; Studio Pro does not). Three defects, not one:

    1. DatabaseType was hardcoded "HSQLDB" — the enum member is Hsqldb, and
      user-supplied values were never validated.
    2. The codec engine read the ports via the SDK names RuntimePortNumber /
      AdminPortNumber while Studio Pro stores HttpPortNumber / ServerPortNumber,
      so the read returned 0 and the overlay wrote it back — any settings write
      silently reset the ports of every configuration.
    3. Mendix renamed the runtime Java version between 11.6 (JavaVersion = "Java21")
      and 11.12 (JavaMajorVersion = "21"); mxcli wrote the 11.6 name unconditionally,
      leaving the real value stale and adding a property 11.12 does not define.

    Enum values are now canonicalised against generated/metamodel and rejected
    otherwise (executor and mxcli check, rule MDL-SET03); version-renamed
    properties are read from and written back to the key the document already carries,
    and never invented.

  • Private constant overrides were corrupted by any settings write.
    Settings$PrivateValue is a marker type with no properties — the value lives on
    the developer's workstation, out of version control. The overlay assumed
    SharedValue and wrote Value: "" into it, producing the same MprProperty
    failure. Since configurations are shared in git, one developer's unrelated edit
    corrupted every developer's private overrides. mxcli now carries the distinction
    (model.ConstantValue.IsPrivate) and preserves it without ever authoring it:
    describe settings emits a comment rather than a re-executable statement,
    alter settings constant … value is refused on a private override, drop constant
    still works, and show constant values reports (private) instead of a blank cell.

Pages

Build and CI

  • 10.24 nightly failure — the MPRv2 Build guard now skips below Mendix 11.6.1,
    where portable app distribution is unavailable.
  • merge=union for .claude/skills/fix-issue.md — every bug fix appends a row to
    one table, so concurrent fixes always collided on the same line. Moving rows from the
    top of the table to the bottom did not help; git's union driver does, and the
    stale guidance claiming otherwise is corrected.

Tunnel hub

  • Previews grouped by Claude Code session, with past sessions retained.
  • Documented that hub credentials belong on the environment, never in the repo.

Verification

Each fix was mutation-checked — reverting it makes its test fail with the reported
symptom — and confirmed end-to-end on real 11.6.6 and 11.12.2 projects with
mxcli docker check reporting 0 errors. The settings fixes were validated by diffing
the Settings$ProjectSettings unit before and after each command against a project
mx create-project produced: the write must be purely additive, and now is on both
versions and both write engines.

Two limits worth stating: Studio Pro is not available in this environment, so the
reported exception could not be reproduced directly — the evidence is byte-level BSON
parity with Studio-Pro-authored output. And the port fix is a read-path workaround
rather than a corrected binding in modelsdk/gen/settings/types.go, whose generator
is not in the repo

claude and others added 25 commits August 1, 2026 09:16
The nightly matrix runs integration tests against 10.24.19.104498, 11.6.6 and
11.12.0. TestBuild_PreservesMPRv2StorageFormat, added with the mendixlabs#808 fix, failed
on the 10.24 row only:

  Build failed: portable app distribution requires Mendix >= 11.6.1,
  found 10.24.19.104498

10.24 is MPRv2 (v2 storage arrived in 10.18), so the test's storage-format
precondition passed and it went on to call Build, which refuses below 11.6.1
before it ever reaches the update-widgets step the test is about. The failure is
a property of the matrix row, not a regression in the fix. The 11.x rows are
unaffected, which is why only one job went red.

Adds the missing precondition. This is a real capability gate rather than a
masked failure: there is no portable app distribution to protect on 10.x, and
TestCheck_PreservesMPRv2StorageFormat has no version guard, so MPRv2
preservation is still covered on every matrix row — it passes on 10.24 today.

Reproduced and verified against real MxBuild installs rather than reasoned about:

  only 10.24 cached, before  ->  FAIL (the exact CI error)
  only 10.24 cached, after   ->  SKIP, and Check still PASSes
  only 11.x cached, after    ->  PASS (39s — the test still genuinely runs)

The last of those matters most: a version gate that silently disabled the test
everywhere would be worse than the failure it fixes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…he repo

MXCLI_HUB_KEY is a credential and a gitignored file wouldn't survive container
recycling anyway — make it explicit that the hub key is set on the Claude Code
environment, not committed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…s#762, mendixlabs#813)

Two DataView properties parsed, passed `mxcli check`, and were then silently
discarded. Different causes, identical symptom.

mendixlabs#762 — FormOrientation: Vertical had no effect on the DEFAULT (modelsdk) engine,
while working under --engine legacy. Studio Pro's "Form orientation" radio has
no BSON field of its own: it IS LabelWidth (0 = Vertical, 3 = Horizontal,
Mendix's metamodel default). Only the legacy writer performed that translation;
the modelsdk writer emitted LabelWidth solely when an explicit `LabelWidth:` was
given, so the orientation was read into the model and dropped. That is the mendixlabs#812
shape again — a field set on the model that no writer on the active engine
reads.

The derivation now lives on the model (pages.DataView.ResolvedLabelWidth) and
both writers call it, so they cannot drift apart again; the modelsdk writer emits
LabelWidth unconditionally.

mendixlabs#813 — showFooter was not settable at all. ShowFooter was only ever set
implicitly, by the presence of a `footer { … }` block, so there was no way to
show an empty footer or to declare footer widgets that start hidden. The property
sits in the validator's allow-list, so it parsed clean and was thrown away. It is
now read explicitly and wins over the block in both directions; hiding a footer
does not discard its widgets.

A trap worth recording: WidgetV3.GetBoolProp is case-SENSITIVE and accepts only a
real bool, unlike GetStringProp which is case-insensitive. My first attempt found
the key and still read false, so the value is now coerced from the looked-up
value and a nonsense one is refused rather than defaulting to false — defaulting
is the failure mode this whole fix is closing.

DESCRIBE emits ShowFooter only when the implicit rule would not reproduce the
stored value, so a plain footer block still round-trips as a block.

Verified end-to-end against a real project, both engines producing identical BSON:

  LabelWidth  [0, 3, 5, 3]           (Vertical, Horizontal, explicit 5, default)
  ShowFooter  [false, false, false, true]

and DESCRIBE round-trips all four forms. Reverting either fix fails the new
tests: 1 case for mendixlabs#762, 6 for mendixlabs#813.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
mendixlabs#760)

Every mxcli-authored page carried a container nobody asked for. The builder
wrapped each non-empty layout placeholder in a Forms$DivContainer named
"conditionalVisibilityWidget<N>", so creating a single button produced a button
AND a container, visible in the Studio Pro widget tree of every page mxcli wrote.

The wrapper was never a BSON requirement. Forms$FormCallArgument carries a
`Widgets` array and a Studio Pro page fills it with its top-level widgets
directly — both writers already emitted an array containing exactly one element.
It existed only because pages.LayoutCallArgument declared a single `Widget`
field, so N widgets had to be squeezed through a 1-widget hole.

Verified against Mendix's own output rather than inferred:
Administration.Account_Overview in a `mx create-project` 11.12.2 app has two
top-level widgets in one placeholder (a DivContainer and a LayoutGrid) and zero
conditionalVisibilityWidget wrappers anywhere.

That also corrects a comment of ours which claimed the wrapper is what "mxcli
(and Studio Pro) adds as a layout placeholder container". Studio Pro uses that
name when wrapping a widget for conditional visibility, not for placeholders —
the same reasoned-by-analogy error that produced mendixlabs#812.

Three places already worked around the container: DESCRIBE unwrapped it to avoid
"a phantom CONTAINER wrapping all widgets", and the catalog builder skipped it as
"transparent". Those readers are deliberately KEPT — projects authored before
this fix still contain wrappers, and must keep reading correctly. This changes
what we write, it does not retro-clean existing pages.

LayoutCallArgument.Widget becomes Widgets []Widget; the builder places the
authored widgets directly. Seven call sites across the two writers, the MCP
backend, the builder and one example.

Verified end-to-end on a real project, identical on both engines:

  one button      -> placeholder widgets [Forms$ActionButton]          0 wrappers
  three widgets   -> [Forms$DynamicText, Forms$ActionButton, Forms$DivContainer]
                     (the DivContainer is the one the author wrote)    0 wrappers
  mx check        -> "The app contains: 0 errors."
  DESCRIBE        -> round-trips to the authored MDL

Reverting the writer's widget list fails the new test with the reported symptom.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…abs#759)

CREATE CONFIGURATION reported success and `mx check` passed, but Studio Pro
threw on the next open of the changed unit:

  System.InvalidOperationException: Sequence contains no matching element
    at ...MprProperty.cs:line 25

mxbuild does not catch this — its deserializer tolerates unknown properties,
while Studio Pro resolves each stored property against the type's property
list. Diffing the Settings$ProjectSettings unit before and after the command,
against a project `mx create-project` produced, found three defects:

1. DatabaseType was hardcoded "HSQLDB". The enumeration member is "Hsqldb";
   nothing matches "HSQLDB". User-supplied values were not validated either,
   so 'postgres' went to disk unchanged.

2. The codec engine read the ports through the gen accessors, which bind the
   SDK names RuntimePortNumber / AdminPortNumber, while Studio Pro stores
   HttpPortNumber / ServerPortNumber. The read returned 0 for both and the
   overlay wrote that 0 back, so every settings write silently reset the ports
   of every existing configuration (Default lost http=8080, server=8090).

3. Mendix renamed the runtime Java version property between 11.6
   ("JavaVersion" = "Java21") and 11.12 ("JavaMajorVersion" = "21"). mxcli
   wrote the 11.6 name unconditionally, so on an 11.12 project a settings
   write left JavaMajorVersion stale and added a JavaVersion property that
   version's metamodel does not define — the property lookup that fails.

Fixes:

- settingsDatabaseType canonicalises the value against the metamodel enum
  (case-insensitively) and rejects anything else, in the executor and in
  `mxcli check` (MDL-SET03, wired through typedSettingsKeys so the existing
  drift guard covers it). New configurations default to Hsqldb and both of
  Studio Pro's ports (8080 / 8090).
- The codec read takes the ports from the stored keys, keeping the gen
  accessors as fallback.
- settingsoverlay.JavaVersionKey / JavaVersion / SetJavaVersion read and write
  the Java version under whichever key the document already carries, and never
  invent one. Both engines use them. The same reasoning removes the hardcoded
  `Tracing: nil` from the no-sibling fallback — 11.12 spells it OpenTelemetry,
  and with no sibling to copy there is no way to tell which is expected.

Verified on real projects at both spellings (11.6.6 and 11.12.2): the
Settings$ProjectSettings diff after `create configuration` is now purely
additive on both, both write engines produce identical output, and
`mx check` passes. Each fix was mutation-checked — reverting it makes the new
test fail with the reported symptom.

Refs mendixlabs#759
A constant override's value is stored one of two ways. Settings$SharedValue
carries a "Value" and lives in the model, so it is in version control and every
developer gets it. Settings$PrivateValue is a marker type with no properties at
all: the value is on the developer's own workstation, deliberately kept out of
the repository (the usual choice for development API tokens).

The overlay assumed every stored SharedOrPrivateValue was a SharedValue and
assigned cv.Value into it. For a private override cv.Value is always "" — the
value is not in the model — so any settings write produced:

  {"$Type": "Settings$PrivateValue", "Value": ""}

a property that type does not define. Studio Pro resolves each stored property
against the type's property list and throws on open:

  System.InvalidOperationException: Sequence contains no matching element
    at ...MprProperty.cs:line 25

the same failure shape as mendixlabs#759. mxbuild does not catch it — `mx check` passes on
the corrupted document. The blast radius is wider than a local mistake:
configurations are shared in version control, so one developer running any
ALTER SETTINGS or CREATE CONFIGURATION corrupted every developer's private
overrides and pushed the result.

Separately, the read type-asserted to *SharedValue, failed, and returned "" with
no way to tell private from empty — so `describe settings` rendered the override
as `value ''`, and replaying describe's own output converted it into a *shared*
empty override, publishing into git a value the developer chose to keep local.

The shared/private choice belongs to the constant; configurations must just
respect it. So mxcli now carries the distinction and preserves it without ever
authoring it:

- model.ConstantValue.IsPrivate, set by both read paths (codec matches on the
  child's type name rather than its Go type, since the registry may decode the
  marker to a bare element.Base).
- The overlay leaves a PrivateValue node byte-identical.
- `describe settings` emits a comment instead of a re-executable statement.
- `alter settings constant ... value ...` on a private override is refused, with
  a pointer to change it in Studio Pro first. `drop constant` is still allowed —
  it discards the whole override, which is what was asked for.

Verified on a real 11.12.2 project with a Studio-Pro-shaped private override:
an unrelated port change leaves the node as {"$Type": "Settings$PrivateValue"}
with no Value key, on both write engines, and `mx check` passes. All four guards
were mutation-checked — reverting each makes its test fail with the reported
symptom.
Follows the private-override fix with the reporting and documentation half.

SHOW CONSTANT VALUES rendered a private override as an empty cell, which is
indistinguishable from an override deliberately set to the empty string. Since
the value genuinely is not in the project, report "(private)" instead.

Documentation, covering each surface the constant/configuration story touches:

- mxcli syntax domain-model.constant.create — a "Shared vs private values"
  section stating the rule and what each statement does with a private override.
  It lives on the constant topic, not the settings topic, because the choice
  belongs to the constant.
- docs-site create-constant.md — the same, as a table. This page already told
  readers to use an empty default "for secrets"; private values are the actual
  mechanism, so it now says so.
- docs-site show-settings.md — what the (private) marker means.
- MDL_QUICK_REFERENCE.md — a note under Constants.
- skills/mendix/project-settings.md — the SHOW CONSTANT VALUES behaviour
  alongside the describe/alter/drop rules.

CLAUDE.md gains the invariant both this bug and mendixlabs#759 come from, since it applies
to any future overlay write and neither `mx check` nor the build catches a
violation: write only keys the document already carries (property names are
version-specific — JavaVersion/JavaMajorVersion, Tracing/OpenTelemetry), and
dispatch a polymorphic child on $Type before assigning fields, because variants
can differ in arity (SharedValue carries a value, PrivateValue is a bare
marker). Plus the sibling trap: validate enum-valued properties against
generated/metamodel rather than passing a user string through.

The (private) marker is mutation-checked.
…lder-wrapper-760

# Conflicts:
#	.claude/skills/fix-issue.md
…guration-759

# Conflicts:
#	.claude/skills/fix-issue.md
…nt-values

# Conflicts:
#	.claude/skills/fix-issue.md
… sessions

The overview was a flat list of live endpoints that vanished ~10 min after a
container went idle. Group them by the Claude Code session that exposed them, and
keep past sessions visible.

Client (`mxcli run --hub`):
- Auto-detects the session id from CLAUDE_CODE_REMOTE_SESSION_ID (fallbacks:
  MXCLI_HUB_SESSION, CLAUDE_CODE_SESSION_ID) and sends it on registration.
  Override with `--hub-session`.

Hub:
- Backends carry a Session; a new /api/sessions groups endpoints by session,
  deriving the claude.ai/code conversation URL from the remote id so each session
  links back.
- A durable SessionLog (cmd/mxcli/tunnelhub/sessions.go) records every endpoint a
  session exposed. On reap/deregister the live entry is stamped into it, so the
  overview keeps offline sessions (state: available → stale → offline). Persisted
  to `--sessions-file` (default ~/.mxcli/hub-sessions.json, survives restarts),
  pruned after `--session-retention` (default 30d). Atomic 0600 writes, mirroring
  the key store.
- Overview page (`/`) regrouped into per-session cards, each linking to its
  claude.ai conversation and listing its endpoints; viewer-scoped like
  /api/backends.

Tests: SessionLog persist+prune+upsert, Registry.Sessions grouping/offline
retention/viewer scoping, sessionURL derivation, client session-id precedence.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…guration-759

# Conflicts:
#	.claude/skills/fix-issue.md
…driver

Every bug fix appends a row to the symptom table in .claude/skills/fix-issue.md,
so two concurrent fixes always collide on the same line — five resolution rounds
in one week. Moving the insertion point from the top of the table to the bottom
did not help: both sides still append to the same place, so the collision simply
moved with it (PRs #76, #77 and #78 all hit it).

git's built-in "union" merge driver keeps BOTH sides of a conflicting hunk
instead of raising a conflict. That is exactly right here: the file is only ever
appended to, and it is looked up by matching a symptom rather than read in order,
so row order carries no meaning.

Verified: two branches that each append a row now merge with no conflict and
both rows present.

Caveat: union applies to the whole file, so two branches editing the same *prose*
line would silently keep both instead of conflicting. The failure is a visible
duplicated line, not corruption. Splitting the table into its own file — so union
covers only append-only content — is the follow-up, deferred because that
restructure would conflict with the three open PRs that all touch this table.
The union-merge commit wrote .gitattributes from scratch, dropping the 53 lines
of line-ending normalization already there (* text=auto, *.go text eol=lf, the
binary declarations). Restore them and append the union rule instead, so the
change is purely additive.
…nt-values

# Conflicts:
#	.claude/skills/fix-issue.md
Every bug fix appends a row to the symptom table, so two concurrent fixes always
collide on the same line — and moving rows from the top of the table to the
bottom did not help, because both sides still append to the same place. git's
built-in union driver keeps both sides instead of conflicting, which is correct
for a file that is only appended to and looked up by matching a symptom.

Appended to the existing rules, not replacing them.
The rationale in fix-issue.md and CLAUDE.md claimed that appending at the end of
the table avoids the conflict two concurrent fixes cause. It does not — both
sides still append to the same line, which is why #76, #77 and #78 each hit it
again after that convention was adopted.

Merging is handled by the merge=union driver in .gitattributes (added in #77).
Appending at the end is kept for readable diffs and rough chronology, which is a
human reason, not a git one.
@ako
ako merged commit 2fb4e67 into mendixlabs:main Aug 1, 2026
4 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants