From dd415939f506f7e69ef3e6cd48e78b5790f65d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:47:41 +0200 Subject: [PATCH] fix(i18n): a refused node-bound field write has a localized sentence (#5600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ui.fieldWriteRefused (en + de) — the sentence BlazorView.UpdatePointer shows when the owning hub refuses a node-bound field write for lack of Update, instead of the raw English UnauthorizedAccessException in the error modal plus an Error log line. The view half lands in MeshWeaver.Plugins (core catalog first). DataBinding.md states the rule: editability is the producing area's decision (Readonly = !canUpdate); a refusal that still happens is a refusal, not a fault. Refs #5600 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/MeshWeaver.Documentation/Data/GUI/DataBinding.md | 1 + src/MeshWeaver.Messaging.Hub/Localization/strings.de.json | 1 + src/MeshWeaver.Messaging.Hub/Localization/strings.en.json | 1 + 3 files changed, 3 insertions(+) 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.",