Every view is data-bound (core: Doc/GUI/DataBinding → Templates first, data later). A node page
in this pack used to subscribe to GetMeshNodeStream() on its hub, wait, and only then build its
controls out of the record. Until the owning hub answered the page was a spinner, and each emission
rebuilt the whole tree. Each converted page now has three parts.
The shape
| Part | What it is |
|---|---|
| Template | BuildTemplate… (pure) returns the whole tree. Every view in it is a control, and every value is a JsonPointerReference into a projection record. A section the record may lack is in the tree anyway; its style is bound and is display: none; when the projection says it is absent. The tree's shape never depends on the data. |
| Projection | Project(…) (pure) turns the node's content into that record (ApiTokenView, ReleaseView, NotificationView). It holds the decisions the builders used to make inline: which sections show, the status label and colour, the toggle's label, the viewer-zone instants. The area returns stream.Select(Project).Bind(_ => template, ViewId), so the framework feeds /data/{ViewId} live. |
| Slots | Two platform pieces still take a LOADED node. The header is MeshNodeLayoutAreas.BuildHeader. The property form is OverviewLayoutArea.BuildPropertyOverview, which picks its fields from the content's runtime type. Each renders in a nested area (NodePageSlots.HeaderArea = PageHeader, NodePageSlots.PropertiesArea = PageProperties) with SpinnerType.Skeleton, so the page around it is already there. When core offers path-bound forms of the two, NodePageSlots is the one place to swap them in. |
A page gated on Read (GroupMembership) keeps its gate as DATA. The access-denied notice and the
body are both in the template. NodePageSlots.ReadGated binds their styles to the viewer's effective
permission.
The bound style hides; it does not withhold. Every viewer receives the body, so a read-gated body holds nothing of the node: only static controls and references to slots. The node's content is served by the slots, and each is an area of its own that a client can ask for with nothing but the node's address. Two things keep it from a viewer without Read:
- The platform refuses the subscription.
SubscribeRequestrequiresPermission.Readon the node, so the viewer is answered "Access denied: user '…' lacks Read permission on '…'" for the page and for each slot. Measured byNodePageSlotsReadGateTeston a mesh with row-level security and no blanket grant. - The slots decide Read themselves.
ReadOnlyHeader,EditableHeaderandPropertiesbuild their piece throughNodePageSlots.ForReader, which returns an empty stack and never calls the builder when the viewer's effective permissions lack Read. This is the decision the old Overview made before it built anything, kept where the content is now built. - Until the node has loaded,
Propertiesemits nothing (NodePageSlots.LoadedForReaderanswers null and the slot holds it back), so the skeleton covers the load. An empty stack there would replace the skeleton with a blank.
Node content in the page
- Instants. A stored instant is UTC and is shown in the viewer's zone (
NodePageSlots.Instant). The zone is read once, on the render turn (NodePageSlots.ViewerZone), and handed to the projection. The projection runs on later emissions of the node stream, where the ambient access context is no longer the viewer's, so reading the zone there shows UTC to everyone. - Links. The release page shows file keys, the NodeType path and the compile-activity path as
markdown links. They are node content, so
ReleaseLayoutAreas.Linkescapes both halves: the text reads as literal characters and the destination cannot close its angle brackets early. - Hrefs. Every node href in these pages (release links and history links, a notification's
target, a membership card) is built by
NodePageSlots.InMeshHref. A path is node content and may begin with/,\or whitespace;"/" + "/evil.example"would be the protocol-relative//evil.example, a link out of the mesh.InMeshHrefstrips that leading run, so the result is always a root-relative path (https://evil.examplebecomes/https://evil.example).
Until the projection arrives, the bound fields draw the platform's loading shape (LoadingShape). A node that has not loaded projects to "every section hidden". A node that loaded without the expected content projects to the notice the page always showed.
Converted
| Page | Areas |
|---|---|
| API token | Overview, Thumbnail |
| Release | Overview |
| Notification | Overview, Thumbnail (a node without a notification falls back to the path-only platform card) |
| GroupMembership | Overview (header and property form in their slots) |
| Access Control (node-wide) | the page; the title is bound to the node name, and the add / advanced sections to the admin probe |
| Groups (node-wide) | the page; the title and "+ Add membership" are bound, and the inherited and local lists render in the GroupsInherited / GroupsLocal slots |
| Group | Overview (the member grid is bound to the live member query); Edit (the bound title, with member rows — one delete action each — in the EditMembers slot) |
| AccessAssignment | Overview (a read gate as data; the detail, with one node-bound editor per role, renders in the AssignmentDetail slot) |
| Activity | Overview (header, progress shape, Cancel / Re-run bound to ActivityView; the log rows and the script's rendered result are a deferred view of the same host), Cancel (bound visibility and disabled state) |
| Versions / VersionDiff (node-wide) | Versions: the back link and title are the frame, and the picker (it shares the From/To selection in this host's /data) is a deferred view beside it. VersionDiff: the back link alone is the frame — the title names the compared versions, so it renders inside the deferred comparison (redline or side-by-side, chosen by what the versions hold); a deployment that retains no history shows its note inside the same frame |
A deferred view beside a static frame is the shape for a body that genuinely computes and has to stay in the page's own host, such as the version picker, whose row buttons write the selection the compare bar reads. Such a body is never the page's ONLY content: the frame renders first.
Not yet
| Unit | Why |
|---|---|
Activity Progress |
This is the code cell's output pane. MeshWeaver.AI.Test (CodeCellOutputCaptureTest) pins its control shape (indicator, log and result as root areas). Its conversion goes with the code-cell surface. |
CodeViews (Overview, RunFromBuffer, edit content) |
The code cell editor and its run loop. Its buffer, staleness and dispatch are pinned by six suites, and it needs its own pass. |
DeleteViews (Delete, StartDelete, FinishQuerySetDelete) |
Delete already emits a loading placeholder first (StartWith). Its reads are the permission gate and the descendant count, which the confirmation states. The other two hits are click-time reads (where to land after a delete). Not converted here. |
Space Overview |
The landing page. Its body markdown carries @@ embeds, and the catalog and header decisions read the content. It needs a pointer-bound body that the document export can still read (core B1, part 2, has the same open point). |
| PartitionSyncAdmin | An admin tab (P3) whose per-partition select buttons are the data's structure. |
PinnedThumbnail |
Verified, not converted: it already draws a card-shaped skeleton before the node arrives. The scanner hit (UpdatePinnedPaths) is the unpin WRITE in a click action. |
Read and left as they are: the AccessAssignment Thumbnail and the GroupMembership Thumbnail.
Every value in the AccessAssignment row is already node-bound: the subject is a path-bound card and
each role is a MeshNodeRoleEditorControl. The row reads the node only to choose its STRUCTURE (how
many role rows there are, and the edit actions the viewer gets). A slot per row would double the
area subscriptions of every access list. The GroupMembership card shows the membership's display name
and its group list. The path-only card reads the node's own Name and Description instead, so it would
say something else. Both need a platform card whose title and abstract bind by pointer.
A page that used to BUILD an admin-only section only for an admin now carries it in the template and binds its visibility. The section and its click actions are therefore in every viewer's tree, and a viewer can click what a style hides. So each action it offers is refused where it executes, never by being hidden:
- A write that runs as the caller is refused by the mesh: a picked subject
(
AccessAssignmentGuard, the access-control pipeline) and the Advanced section's partition policy (IMeshService.CreateNodeas the clicking user). Measured through the real clicks: a viewer is answered "Access denied: Create permission required …" for the subject and an error dialog for the policy, and neither the assignment nor_Policyexists afterwards. - A write that runs as System passes no access check of its own, so the action checks the
caller itself. The email grant is the one on this page. For an email with no account,
SpaceInviteServicewrites the Invitation and the deferred grant as System, and the grant later lands as System too.AccessControlViews.GrantByEmailAstherefore runs only for a caller holding the manage-access permission (Delete) on the scope, read on the click's own turn, and refuses everyone else, an unknown caller included. Without that check a viewer of the page could make an address of their choosing Admin on the node (measured: the unchecked call answersInvited).
The Group Edit page's "Invite by Email" is the same shape and was open before this change set: the
Edit area is served to every viewer who may READ the group, and for an address with no account
InviteToGroup writes the Invitation and the deferred add-to-group — membership and role — as
System. GroupLayoutAreas.InviteAllToGroupAs (landed on main by MeshWeaver.Plugins#2710) runs
only for a caller who may add members and manage access on the group (Create and Delete). Measured through the real click in
GroupInviteAuthorizationTest: before the check a reader's click answered "1 invited … role Admin"
and left both records; with it the reader is refused and leaves nothing, and the admin's clicks
invite as before.
The same holds for a slot a read gate hides: AssignmentDetail builds through
NodePageSlots.ForReader. Before the projection arrives, a section with a bound style draws the
loading shape — a text-free pulse with pointer input off (LoadingShape).
The two Group pages publish under two ids, groupView and groupEditView: one id is one shape.
GroupView compares its member rows by content, so the Overview republishes only when what it
shows changed. A group's description is user text and is shown by a label, which renders its value
as text.
The text these pages used to carry inline now lives in the module-owned table NodeChromeTexts
(English and German). Strings that were already catalog keys stay catalog keys.
Pinned by
src/MeshWeaver.Graph.Views.Test/NodeChromeTemplatesTest.cs, AccessTemplatesTest.cs and
AccessControlLayoutAreaTest.AccessControlAndGroups_AreTemplates_… check these things:
- Every template is static, and every pointer in it names a property of its projection.
- The header and property-form slots are skeleton slots.
- The projections make the decisions they replaced.
NodeChromeProjectionsTest: a hostile file key or path renders, through the platform's markdown pipeline, as the text of its own link and nothing else; an API token node that has not loaded hides every section, and only a loaded node without a token shows the notice; stored instants render in the viewer's zone across both DST changes and the date rollover;ForReaderbuilds nothing without Read;LoadedForReaderemits nothing until the node has loaded; a node path beginning with/,//,\or a scheme stays an in-mesh href on every page.NodePageSlotsReadGateTest(live, row-level security, no blanket grant): a viewer without Read who asks for the GroupMembership Overview,PageHeaderorPagePropertiesis refused and receives nothing of the membership, and one who asks for a Notification's read-onlyPageHeaderreceives nothing of the node. The node's admin draws each slot through the same subscriptions.AccessPagesAuthorizationTest(live, row-level security, no blanket grant): a viewer without Read is refused the AccessAssignment Overview and itsAssignmentDetailslot and receives nothing of the assignment; a viewer who fills in the email field and clicks "+ Add" is refused and leaves no invitation and no deferred grant, while the admin's identical click schedules the grant; a viewer who picks a subject and clicks "+ Add", or saves the Advanced section's partition policy, is refused and leaves no assignment and no_Policy, while the admin's identical clicks write both. Falsified: with the email check bypassed, or with either create run as System, exactly the viewer arms fail.AccessTemplatesTest: the group projection reads content that arrives as a JSON frame, hands the description over verbatim to a label, and is equal to another projection of the same group.- Live: the rendered Notification Overview's first control is the template bound to
/data/notificationView, and its projection follows an edit of the node. The GroupMembership Overview publishes its read gate, and both slots draw. The Group member grid follows a membership created while the page is open. Access Control and Groups bind the node name and the admin probe. The Activity Overview follows a run from Running to Succeeded. Versions and VersionDiff render their frame beside a deferred body (ActivityAndVersionTemplatesTest,VersionViewsTest.VersionsAndDiff_RenderTheirFrame_…).
Negative control: adding one deferred view to a template fails EveryViewIsStatic.
Deploy
These views are compiled into the pack's assembly, so a new activation picks them up. A node hub
that is already running keeps the old views until it is disposed. After the roll, recycle the
NodeTypes ApiToken, Release, Notification, GroupMembership, Group, AccessAssignment and Activity;
the dispose cascades to their live instances. The node-wide Access Control, Groups and Versions areas reach a
node when that node's hub is next activated.