diff --git a/src/MeshWeaver.Documentation/Data/GUI/DataBinding.md b/src/MeshWeaver.Documentation/Data/GUI/DataBinding.md index 679f25b7ac..c903a0cc47 100644 --- a/src/MeshWeaver.Documentation/Data/GUI/DataBinding.md +++ b/src/MeshWeaver.Documentation/Data/GUI/DataBinding.md @@ -198,6 +198,7 @@ Mechanics (so you know what's load-bearing): - Field-pointer resolution against the node is **case-insensitive**, so a metadata DTO's PascalCase pointer (`Name`, `Description`) and a content editor's camelCase pointer (`harness`, `messageContent`) both bind without the layout area knowing the JSON casing. - **Edit-state stays in `/data`.** The click-to-edit toggle (`editState_…`) is transient view state, not node content — it is never written to the node. Only field VALUES are node-bound. - A few read-only display controls (the `[Dimension]` / options / formatted-date toggle *labels*) derive their text from the layout-area `/data` stream rather than a value pointer. When you node-bind a toggleable form, keep `/data/{dataId}` as a **one-way live projection** of the node content (`GetMeshNodeStream(path).Select(n => n.Content).Subscribe(c => host.UpdateData(dataId, c))`) so those labels stay correct. This is a pure read mirror — it follows the node, has no save loop, and never writes back, so it is NOT the forbidden replicate-then-save pattern. +- **🚨 Editability is the PRODUCING AREA's decision, and it is part of the structure.** A node-bound field writes as the viewer, and the owning hub refuses a viewer without `Update` on the node. So the area that emits the control resolves `hub.GetEffectivePermissions(path)` and sets `Readonly = !canUpdate` (or `CanEdit` on the editors that carry it) — never hands a public visitor a box that invites typing its write can only refuse. The course wish books are the reference shape (`AgenticPrimer/WishBook` → `WishBookAreas.Write`, Education#363). **A write that is refused anyway** (grants changed while the page was open, or an area that forgot the check) reaches the view as an `UnauthorizedAccessException`; `BlazorView.UpdatePointer` classifies it as a REFUSAL, not a fault: the person sees the localized `ui.fieldWriteRefused` sentence and the log carries a Warning, instead of the raw English exception in the error modal and an Error line the incident filer turns into a ticket (#5600). ## Anti-patterns — never do these diff --git a/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json b/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json index e5725f2caa..ad4dd8837a 100644 --- a/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json +++ b/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json @@ -806,6 +806,7 @@ "ui.recycleFailed": "Der Hub unter '{0}' hat nach dem angeforderten Neustart noch nicht wieder geantwortet: {1}", "ui.mdFillFieldsGeneratePlan": "*Felder ausfüllen und auf **Plan erzeugen** klicken.*", "ui.accessDeniedPlain": "Zugriff verweigert.", + "ui.fieldWriteRefused": "Nicht gespeichert: Sie dürfen diese Seite lesen, aber nicht ändern.", "ui.accessDeniedAdminsOnly": "Zugriff verweigert — nur für Plattform-Administratoren.", "ui.autoSaved": "Änderungen werden automatisch gespeichert", "ui.connectRepoFirst": "Zuerst im Tab **GitHub Sync** ein GitHub-Repository verbinden.", diff --git a/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json b/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json index 891e423392..c4debc084e 100644 --- a/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json +++ b/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json @@ -806,6 +806,7 @@ "ui.recycleFailed": "The hub at '{0}' has not answered again yet after the recycle was requested: {1}", "ui.mdFillFieldsGeneratePlan": "*Fill in the fields and click **Generate plan**.*", "ui.accessDeniedPlain": "Access denied.", + "ui.fieldWriteRefused": "Not saved: you can read this page, but you may not change it.", "ui.accessDeniedAdminsOnly": "Access denied — platform admins only.", "ui.autoSaved": "Changes are saved automatically", "ui.connectRepoFirst": "Connect a GitHub repository in the **GitHub Sync** tab first.",