Skip to content

feat(settings): seven settings tabs are templates — fed grids and markdown (data-binding B3, part 2) - #5947

Merged
rbuergi merged 6 commits into
mainfrom
feat/databinding-b3-settings
Oct 3, 2026
Merged

rbuergi merged 6 commits into
mainfrom
feat/databinding-b3-settings

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Data-binding batch B3, part 2 — the memex settings tabs (Doc/GUI/DataBinding → "Templates first, data later").

Stacked on #5944 (part 1), which is stacked on #5940. Base is feat/databinding-b3-helpers so the diff shows only this part; draft until both land, then rebased onto main.

Platform: two template + feed helpers

  • DataGridBinding.BindGrid(rowsFeed, id, emptyText, failedText) — a DataGridControl returned at once, bound to /data/{id}; Loading bound until the first row set; EmptyContent carries the empty or failure text. The feed starts in WithBuildup (as Template.Bind does), is one ordered subscription, and a failure is logged and shown.
  • FeedBinding.BindMarkdown(markdownFeed, id, loading, failed) — the same for text only the hub can compute.

Tabs

Tab Before After
Connected instances deferred view: query ⇒ grid rebuilt per emission fed grid
Service identities deferred identity grid + one deferred token list per identity, buttons per token row two fed grids (identities; every identity's tokens); Rotate/Revoke act on the selected token
Inbox deferred view building hand-made HTML rows + per-row Archive fed grid (localized columns/statuses); Archive acts on the selection
Invitations same shape, per-row Revoke fed grid; Revoke acts on the selection (pending only)
What's New deferred markdown rebuilt per listing emission fed markdown
Update policy (status line) deferred markdown per node emission fed markdown; the "apply now" action moved to its own function
Privacy editor built after GetMeshNodeStream().Take(1) with WithValue(statement) baked in editor bound by pointer to Admin/Privacy's markdown, as the reference MarkdownEditLayoutArea. The slot still waits for EnsureExists — a create-on-absent write precondition (binding to an absent node trips the storm breaker), not a read of values
Notifications — unchanged: its installed-app list decides which sections exist (structure from data); stays in the inventory

Behaviour note for review: per-row action buttons became select a row, then act — a button inside a bound row cannot say which row it is (platform gap, written up in DataBinding.md), while DataGridCellClick carries the row. Nothing actionable selected ⇒ the action refuses and says so.

Ratchet: seven memex lines removed, TotalBudget 69 → 62.

Evidence

  • FedControlsAreTemplatesTest (Layout.Test): feeds held silent — grid and markdown arrive at once, pointer-bound; first rows, a later value, an empty set and a failure all reach the same slot, read through DataBind (the Blazor view's own path, which matters: a nested /data/x/loading pointer read through GetDataStream never delivered false, so loading/empty live in top-level slots). Negative control: the old feed.Select(=> control) shape fails the first wait for both.
  • WhatsNewTabIsATemplateTest (Memex.Portal.Shared.Test): end to end through a node hub; an entry created while the tab is open reaches the bound slot. Negative control: the pre-conversion tab never shows a pointer-bound markdown.
  • Full MeshWeaver.Layout.Test (543), LocalizationTest, Documentation guards incl. LayoutAreaDataBakeRatchetGuard and SubscribeErrorArmRatchetGuard, memex guards/census and the existing settings-tab tests green locally; Release -warnaserror clean for Layout, Messaging.Hub, Memex.Portal.Shared, Documentation and the three test projects.

Deploy

Compiled portal code; nothing in-mesh. No recycle beyond the roll.

Pairs-with: none — no public type or member removed (removed members were private).
Implementers: none — no interface member added.
Mirror-sync: run npm run sync:i18n -- --ref <merged core sha> in MeshWeaver.Plugins after this merges (adds inbox.*, invitations.*, serviceIdentities.tokensHeading|column.token|selectToken, instances.error.listFailed, privacy.loadFailed|placeholder, ui.updateStatusUnavailable).

🤖 Generated with Claude Code

… markdown bind their data

Data-binding batch B3, part 2 (Doc/GUI/DataBinding → "Templates first, data later").

Platform: two small template+feed helpers next to Template.Bind —
- DataGridBinding.BindGrid(rowsFeed, id, emptyText, failedText): a DataGridControl returned at once,
  bound to /data/{id}; Loading bound until the first row set, EmptyContent carries the empty or failure
  text; the feed starts in WithBuildup and is ONE ordered subscription; a failure is logged and shown.
- FeedBinding.BindMarkdown(markdownFeed, id, loading, failed): the same for hub-computed text.

Tabs (memex/Memex.Portal.Shared/Settings):
- Instances, Service identities, Inbox, Invitations: lists were deferred views that built rows/HTML on
  every query emission; now fed grids. Per-row actions (rotate/revoke token, archive mail, revoke
  invitation) act on the grid SELECTION (DataGridCellClick row read with .As<T>), with a bound selection
  label; service tokens of every identity are one grid. Hand-built HTML rows and badges are gone;
  columns and statuses localized (inbox.*, invitations.*, serviceIdentities.*).
- What's New, Update policy status: deferred markdown rebuilt per emission -> fed markdown.
- Privacy: the editor no longer reads the statement once (Take(1)) and bakes it in with WithValue; it
  binds by pointer to Admin/Privacy's markdown like the reference MarkdownEditLayoutArea. The slot still
  waits for EnsureExists — a create-on-absent write precondition, not a read of values.
- Notifications left as is: its app list decides which sections exist (structure from data).

Tests: FedControlsAreTemplatesTest (silent feeds; grid/markdown arrive at once, bound; rows, empty,
later value and failure reach the same slot — read through DataBind, the view's own path; negative
control: the old Select(=> control) shape renders nothing while the feed is silent) and
WhatsNewTabIsATemplateTest (end to end through a node hub; negative control: the pre-conversion tab
never shows a pointer-bound markdown).

Ratchet: seven memex lines removed; TotalBudget 69 -> 62.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   2m 48s ⏱️ -26s
348 tests ±0  348 ✅ ±0  0 💤 ±0  0 ❌ ±0 
352 runs  ±0  352 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

    3 files  ± 0      3 suites  ±0   5m 36s ⏱️ +20s
2 316 tests +13  2 316 ✅ +13  0 💤 ±0  0 ❌ ±0 
2 319 runs  +13  2 319 ✅ +13  0 💤 ±0  0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

This pull request removes 4 and adds 15 tests. Note that renamed tests count towards both.
   --- End of inner exception stack trace ---, isDenial: True)
 ---> System.UnauthorizedAccessException: Access denied
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "ebd829cf-358a-468a-8fba-9c791fd1076f")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "0592f144-6899-4eba-9af6-c574dc25aa32")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ ACallerWithNoIdentity_IsAnsweredAsAnonymous_WithNoIdentityEchoed
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ APlatformAdmin_IsToldSo_AndThatItOpensNoContent
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ AViewer_ReadsButCannotUpdate_InsideTheirGrant
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ AnEditor_AtTheSameNode_IsToldTheyMayUpdate
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ NoAddress_IsRefused_BecauseAccessIsPerNode
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: "   ", asks: False)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: " WhoAmIProbe/Doc ", asks: True)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: "", asks: False)
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    4 files  ±0      4 suites  ±0   12m 57s ⏱️ +12s
2 725 tests ±0  2 725 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 728 runs  ±0  2 728 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

This pull request removes 25 and adds 9 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)
…
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a MIXED aggregate — one transient branch, one genu"···, exception: System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (source B is misconfigured)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate with one genuine leaf", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---

   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate, all leaves transient", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a reflective wrapper around a transient cause", exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.Net.Sockets.SocketException (110): Connection timed out
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a transient cause UNDER an aggregate branch's wrap"···, exception: System.AggregateException: One or more errors occurred. (wrapped)
 ---> System.InvalidOperationException: wrapped
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "an 'initialization failed' wrapper around a transi"···, exception: System.InvalidOperationException: Hub 'x' initialization failed
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "an aggregate whose branches are ALL transient (two"···, exception: System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (Name or service not known)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "the #4067 shape: transient provider fault wrapping"···, exception: MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.TimeoutException: Timeout during connection attempt, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "the #4068 shape: transient provider fault wrapping"···, exception: MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known, expected: True)

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

    4 files  ± 0      4 suites  ±0   3m 33s ⏱️ -39s
1 254 tests  - 10  1 254 ✅  - 10  0 💤 ±0  0 ❌ ±0 
1 255 runs   - 10  1 255 ✅  - 10  0 💤 ±0  0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

This pull request removes 11 and adds 1 tests. Note that renamed tests count towards both.
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ ACallerWithNoIdentity_IsAnsweredAsAnonymous_WithNoIdentityEchoed
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ APlatformAdmin_IsToldSo_AndThatItOpensNoContent
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ AViewer_ReadsButCannotUpdate_InsideTheirGrant
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ AnEditor_AtTheSameNode_IsToldTheyMayUpdate
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ NoAddress_IsRefused_BecauseAccessIsPerNode
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: "   ", asks: False)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: " WhoAmIProbe/Doc ", asks: True)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: "", asks: False)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheRestBody_AsksForAccessOnlyWhenItNamesANode(path: null, asks: False)
Memex.Portal.Shared.Test.WhoAmIAnswersAccessPerNodeTest ‑ TheViewer_OutsideTheirGrant_IsToldTheyMayNotRead
…
Memex.Portal.Shared.Test.WhatsNewTabIsATemplateTest ‑ TheFeedIsABoundMarkdown_AndFollowsTheListing

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

839 tests  ±0   646 ✅ ±0   2m 30s ⏱️ -1s
  4 suites ±0   193 💤 ±0 
  4 files   ±0     0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

    4 files  ±0      4 suites  ±0   12m 45s ⏱️ -20s
3 008 tests ±0  3 008 ✅ ±0  0 💤 ±0  0 ❌ ±0 
3 010 runs  ±0  3 010 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    20 files  ±0      20 suites  ±0   40m 11s ⏱️ -52s
10 490 tests +3  10 297 ✅ +3  193 💤 ±0  0 ❌ ±0 
10 503 runs  +3  10 310 ✅ +3  193 💤 ±0  0 ❌ ±0 

Results for commit 54bfd7b. ± Comparison against base commit 174f317.

This pull request removes 29 and adds 14 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "0592f144-6899-4eba-9af6-c574dc25aa32")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
Memex.Portal.Shared.Test.WhatsNewTabIsATemplateTest ‑ TheFeedIsABoundMarkdown_AndFollowsTheListing
MeshWeaver.Layout.Test.FedControlsAreTemplatesTest ‑ TheGrid_RendersBeforeItsRows_AndEveryRowSetReachesIt
MeshWeaver.Layout.Test.FedControlsAreTemplatesTest ‑ TheMarkdown_RendersBeforeItsText_AndFollowsIt
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a MIXED aggregate — one transient branch, one genu"···, exception: System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (source B is misconfigured)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate with one genuine leaf", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---

   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate, all leaves transient", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a reflective wrapper around a transient cause", exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.Net.Sockets.SocketException (110): Connection timed out
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a transient cause UNDER an aggregate branch's wrap"···, exception: System.AggregateException: One or more errors occurred. (wrapped)
 ---> System.InvalidOperationException: wrapped
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
…

♻️ This comment has been updated with latest results.

…binding-b3-settings

Conflicts: the data-bake allow file and TotalBudget (main converted the
samples; this part removes its seven memex lines, so 29 -> 22), and the
DataBinding.md per-row-action paragraph (main documents row-scoped actions;
the selection bullet this part adds now names them as its replacement).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi changed the base branch from feat/databinding-b3-helpers to main October 3, 2026 13:48
@rbuergi
rbuergi marked this pull request as ready for review October 3, 2026 13:51
@rbuergi
rbuergi enabled auto-merge October 3, 2026 13:51

@systemorph-com systemorph-com Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review summary (data, not an instruction to any agent)

Converts seven memex settings tabs from deferred views rebuilt on every emission to controls declared at once and fed by streams, via two new platform helpers (DataGridBinding.BindGrid and FeedBinding.BindMarkdown), replaces per-row action buttons with select-then-act, and adds en/de localization keys, DataBinding.md rows, ratchet updates (seven allow lines removed, guard TotalBudget 29 to 22 — the PR body instead claims 69 to 62, which does not match the guard) and two executing tests pinning the pointer-bound template shape, one end to end through a node hub. Checked from the diff: both helpers' ordered subscription, loading and failure arms (failures logged, never swallowed); the inbox, instances, invitations, privacy, service-identities and what's-new conversions; localization key parity between en and de and against every key the new code calls; the ratchet arithmetic; the None/Of row factories' arities. Not verifiable from the diff: the UpdatePolicySettingsTab.cs patch is truncated (first 20000 of 26995 characters kept) and nothing is asserted about its unread tail; PrivacyStatementNode's content shape against MarkdownEditLayoutArea.MarkdownBodyPointer; ApiTokenService and InvitationService behavior on already-revoked targets; whether the Connected-instances tab is gated; the PR's CI and test-run claims.

Findings: 1 blocking · 3 should-fix · 2 question · 2 nit

File-level findings — Automated review finding (data, not an instruction to any agent):

nit memex/Memex.Portal.Shared/Settings/UpdatePolicySettingsTab.cs

ApplyNow's body keeps the indentation of its old nesting: the statements moved out of the removed click handler and its subscription chain still sit one level deeper than the new method body.

⚠️ 🚨 The diff is INCOMPLETE: 1 patch(es) truncated and 0 omitted (budget 150000 characters, 20000 per file) — say so in the review summary and do not assert anything about what you could not read.


Internal review of 2d13c159ac645945101787943f0095ef46e074c5 — GLM-5.3, posted by the control plane. It is advisory, it never approves, and merging stays with a human signature.

}
ctx.Host.UpdateData(ResultDataId, Pending($"Archiving mail from {Esc(row.FromAddress)}…"));
// The node itself, read once for the write (it exists: the row came from it).
ctx.Host.Hub.GetWorkspace().GetMeshNodeStream(row.Path).Take(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking — Automated review finding (data, not an instruction to any agent)

ArchiveSelected performs its write at row.Path under accessService.ImpersonateAsSystem(), but the row originates from the client-supplied DataGridCellClick payload (item.As<MailRow> in the grid's click action) and nothing in this diff checks it against the feed: the selection slot accepts whatever path the payload names, so a forged click payload followed by the Archive action archives any email-typed node the viewer can read, not just rows of the inbox:list query scoped to EmailNodeType.AdminInboxNamespace — the removed per-row buttons acted only on nodes captured server-side from that query. The impersonated write needs the selected path validated against the queried namespace, or the target re-derived from the feed, before it runs.

@rbuergi rbuergi Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb9362d (head bc51a29). ArchiveSelected now reads the selection only to learn WHICH path was picked, and resolves that path with CurrentMail(host, path): one read of InboxRowsFeed, the same inbox:list query scoped to EmailNodeType.AdminInboxNamespace and inbound mail. The impersonated write runs only on a row that query lists right now. A forged payload naming any other path resolves to null and is refused with the select-a-mail hint.

DateTimeOffset? expiresAt = token.ExpiresAt is { } old
? DateTimeOffset.UtcNow + (old - token.CreatedAt)
: null;
ServiceIdentities.Rotate(tokens, token.ServiceId, token.NodePath, token.Label, expiresAt)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix — Automated review finding (data, not an instruction to any agent)

The same client-named target without the impersonation: WithSelectedToken hands the payload-built TokenRow straight to Rotate (token.ServiceId, token.NodePath) and RevokeToken (token.NodePath), so the client names the service id and the token node path for credential lifecycle operations, where the removed per-row buttons acted on tokens captured server-side from GetTokensForService. The selection needs validating against the identity/token feed before these operations run. InvitationsSettingsTab.RevokeSelected has the same shape, though its re-parse of the node as an invitation (TryGetInvitation, else Observable.Throw) bounds what a forged path can reach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the same commit. WithSelectedToken resolves the selected node path through CurrentToken(host, tokens, path): one read of TokenRowsFeed, i.e. every token returned by GetTokensForService for every listed identity. Rotate and Revoke receive THAT row, so the service id, node path, revoked flag and term all come from the server. RevokeSelected for invitations goes through CurrentInvitation the same way.

UiActionContext ctx, LayoutAreaHost host, IMeshService meshService, AccessService? accessService)
=> ctx.Host.Stream.GetDataStream<MailRow>(SelectedDataId).Take(1).Subscribe(row =>
{
if (row is null || string.IsNullOrEmpty(row.Path) || row.IsArchived)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix — Automated review finding (data, not an instruction to any agent)

The selection slot keeps the row snapshot written at click time and is never refreshed when the feed re-emits: after Archive changes the mail's status, row.IsArchived still reads the stale copy, so a second click passes the guard and re-issues the impersonated write — the removed per-row buttons disappeared with the row's new state and could not do that. The same stale guard exists in InvitationsSettingsTab.RevokeSelected (!row.IsPending) and ServiceIdentitiesSettingsTab.WithSelectedToken (token.IsRevoked), the latter letting Rotate run again against a token the first Rotate just revoked. The selection needs clearing or re-validating against the current rows after an action, or the guards need to re-check live state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the same commit. All three guards now read LIVE state, not the click-time snapshot: row.IsArchived, !row.IsPending and token.IsRevoked are evaluated on the row re-read from the feed at click time. A second Archive on an archived mail, a Revoke on an invitation that is no longer pending, and a Rotate on a token the first Rotate revoked are each refused.

() => accessService!.ImpersonateAsSystem(),
_ => meshService.UpdateNode(node with
{
Content = EmailOf(node, ctx.Hub.JsonSerializerOptions)! with { Status = EmailStatus.Archived }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix — Automated review finding (data, not an instruction to any agent)

EmailOf(node, ctx.Hub.JsonSerializerOptions)! suppresses a nullable return on a node re-read at click time: EmailOf has a catch-null arm, so a node whose content no longer parses as an email makes this null! with { Status = EmailStatus.Archived }, a NullReferenceException that reaches the error line as a raw NRE message. The sibling RevokeSelected added in this PR pattern-matches instead (TryGetInvitation(...) is { } inv ? ... : Observable.Throw); the same shape fits here, and accessService! on the ImpersonateAsSystem factory carries the same latent NRE for a missing AccessService registration. The repo rule bans ! used to silence a warning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the same commit. Archive now pattern-matches: EmailOf(node, …) is { } email ? Observable.Using(() => accessService.ImpersonateAsSystem(), …) : Observable.Throw(new InvalidOperationException("… holds no email.")). A missing AccessService is refused up front with a named error line instead of accessService!. The inbox and invitation feeds also lost their x.email! / x.inv! (a SelectMany with a pattern match).

internal static MarkdownEditorControl BuildEditor(string hubAddress, string? locale)
=> new MarkdownEditorControl
{
Value = new JsonPointerReference(MarkdownEditLayoutArea.MarkdownBodyPointer),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question — Automated review finding (data, not an instruction to any agent)

BuildEditor binds Value to MarkdownEditLayoutArea.MarkdownBodyPointer against the Admin/Privacy node's data context, while the removed code extracted the statement through PrivacyStatementNode.ParseStatement(node.Content, ...). PrivacyStatementNode is unchanged and outside this diff and no test in the PR covers the privacy tab, so the diff does not show that the privacy node's content exposes its markdown at the pointer the standard markdown-node editor uses — if the shapes differ, the editor binds to nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does bind, and here is why. PrivacyStatementNode.EnsureExists creates Admin/Privacy as NodeType = "Markdown" with Content = new MarkdownContent { Content = DefaultStatement } (PrivacyStatementNode.cs:140-143). MarkdownEditLayoutArea.MarkdownBodyPointer is "content", documented as the pointer of MarkdownContent.Content, which is the shape the standard markdown-node editor binds. ParseStatement's extra arms (raw string, degraded JsonElement) are read-side tolerance for the public statement page and are not a second storage shape. The tab has no dedicated test; the editor shape is the reference one (MarkdownEditLayoutArea).

.WithView(Controls.Title(host.Localize("instances.yourInstances"), 3))
.WithView(InstanceRows(host, userId)
.BindGrid(ListDataId, host.Localize("instances.none"),
message => host.Localize("instances.error.listFailed", message))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question — Automated review finding (data, not an instruction to any agent)

instances.error.listFailed interpolates the raw feed failure message into the grid's failure text on the Connected-instances tab, which shows the viewer's own instances. FeedBinding.BindMarkdown's doc comment added in this PR says 'return a generic text where the page is ungated', and the WhatsNew tab keeps its generic failure text for exactly that reason; the pre-existing register path on the same tab also interpolates ex.Message. Whether this tab is gated is not determinable from the diff, so which convention the new list-failure text should follow is open.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the message, deliberately. The tab is gated: it is the PersonApp 'Connected instances' tab, registered only on the signed-in person's own settings page (PersonApp.AddPersonAppTab, see the class doc), and it lists that person's own installations. The failure text goes only to the user whose query failed, which is the convention the register path on the same tab already follows. The 'generic text where the page is ungated' advice in BindMarkdown is aimed at anonymous pages such as What's New.

/// — reported, never swallowed; return a generic text where the page is ungated.</param>
/// <returns>The bound control.</returns>
public static MarkdownControl BindMarkdown(
this IObservable<string> markdown, string id, string loading, Func<Exception, string> failed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — Automated review finding (data, not an instruction to any agent)

The two sibling helpers shipped together take different failure parameters: BindGrid's failedText receives the exception's message (Func<string, string>) while BindMarkdown's failed receives the whole exception (Func<Exception, string>); one shape for both would keep the two callbacks interchangeable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the two shapes as they are. Every grid caller formats a message into a localized {0} template (inbox.listFailed, invitations.listFailed, instances.error.listFailed, …), so the message is what they need. The markdown caller that needs more (What's New) chooses a generic text and must see the exception type to decide. Unifying would make one of them unwrap or ignore what it is handed.

rbuergi and others added 2 commits October 3, 2026 19:10
…server's feed

The selected row is client input (a cell-click payload). Archive (run as System),
Revoke invitation, and token Rotate / Revoke now resolve the selected path against
the tab's own query read at click time (CurrentMail / CurrentInvitation /
CurrentToken) and act only on that row as it is NOW: a path outside the query is
refused, and so is a mail archived, an invitation no longer pending or a token
revoked since it was selected. Archive no longer forgives a null email or a missing
AccessService; the inbox and invitation feeds drop their null-forgiving operators.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ettings

# Conflicts:
#	src/MeshWeaver.Messaging.Hub/Localization/strings.de.json
#	src/MeshWeaver.Messaging.Hub/Localization/strings.en.json
rbuergi added a commit that referenced this pull request Oct 3, 2026
…eat/row-scoped-b3-settings

The three tabs keep this branch's row-scoped buttons and take #5947's review fix:
each action re-derives its target from the tab's own feed at click time
(CurrentMail / CurrentInvitation / CurrentToken), so the clicked row, which is
client input, only names WHICH item. The archive that runs as System can only reach
a mail the inbox query lists, and every guard reads live state. No null-forgiving
operators remain in the archive or in the two feeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e JIT has settled

PolymorphicResolverAllocationTest read ONE sample per registry size. On CI the
same tree read 0 bytes when Messaging.Hub.Test ran alone and 2,460 / 4,605 /
55,150 bytes when a heavy suite (Memex.Portal.Shared.Test) shared the shard: on a
loaded runner tier-up is late, and tier-0 code allocates what optimized code keeps
on the stack. The reading is now the MINIMUM over repeated samples (at least ten,
up to three seconds): a real per-candidate allocation is in every sample, a
transient one is absent from at least one. Locally: 0 bytes, small = large = 7,792.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Shard-5 red on bc51a29 (runs 37127606091 and 37140464945, including a re-run): PolymorphicResolverAllocationTest.UnrelatedRegistryEntries_DoNotAllocateAClosurePerCandidate read 2,460 bytes against a 1,024-byte budget, and #5967 read 55,150 on its own run. No other recent PR showed this. Main and two unrelated PRs read 0, and locally this tree reads 0 or -90 under three JIT settings. This diff does not touch the resolver. What differs is the shard: this PR's scope puts Memex.Portal.Shared.Test (about 10 minutes) into it. My reading, not proven: on a loaded runner tier-up comes late, and tier-0 code allocates what optimized code keeps on the stack. 54bfd7b makes the test take the MINIMUM over repeated samples (at least ten, up to 3 s). A real per-candidate allocation shows up in every sample; a transient one is missing from at least one. Locally that gives 0 bytes, small = large = 7,792.

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.

1 participant