Repository navigation
The portal image ships no Blazor view pack — a fresh mesh renders every control as escaped JSON, and reports healthy while doing it #2548
Description
Symptom
On a mesh that has not installed the view packs from a registry, every layout-area control renders as escaped JSON instead of a view. The page body is literally:
NamedAreaControl { Id = , Style = , Readonly = , Skins = System.Collections.Immutable.ImmutableList`1[MeshWeaver.Layout.Skin],
Class = , IsClickable = False, DataContext = , PageTitle = , Meta = , ModuleName = MeshWeaver.Layout,
ApiVersion = 1.0.0, Area = Overview, ShowProgress = , ProgressMessage = , SpinnerType = Ring }
That is character-for-character BlazorViewRegistry.cs:90 — Controls.Html(WebUtility.HtmlEncode(instance.ToString())), the WithFallbackView slot wired at line 60.
The portal boots healthy and the rollout completes. That is the worst part: Modules:Required does not name the view packs, so nothing reports the loss.
Mechanism
db552ffbf (2026-08-25, "Blazor view definitions leave the platform: DefaultViews and GraphViews move out") removed, in one commit:
- the two
MeshModuleClosurerows frommemex/MeshModulesPublish.targets→ nomodules/<Name>copy in the image; - their
Modules:Assembliesentries → nothing activates them.
Removal (2) was deliberate and is the point of the commit — quoting it:
the removal is the operative one:
ComputeEffectiveModuleEntriestakes baseline entries first and dedupes the persisted store entry by name, so while a baseline entry named these packs it shadowed the installed copy. With it gone, the registry-landed pack is [used].
That reasoning is correct. The gap is that (1) went with it and no host re-added it, so the packs are now in neither place:
- MeshWeaver
main: noBlazor.Views/Blazor.Graphclosure row anywhere. - MeshWeaver.Plugins hosts:
Memex.Portal.Distributed/Memex.Portal.Monolithadd closure rows for Cosmos and Snowflake only. - Shipped
appsettings.json:Modules:Assemblies = [MeshWeaver.AI.dll, MeshWeaver.Hosting.Grpc.dll];Modules:Requirednames Radzen/Analysis/EntityViews/GoogleMaps/Speech/Social/AI — neither view pack. - Enumerating all 18,399 files in
memex-portal-ai@sha256:85a6de78…(3.0.0-rc8.ci.6141): noMeshWeaver.Blazor.Views.dll, noMeshWeaver.Blazor.Graph.dll— not in/app, not underapp/modules/.
MeshWeaver.Blazor keeps only the machinery and the fallback slot (its own comment, BlazorViewRegistry.cs:55-59); the sole NamedAreaControl → NamedAreaView map lives in MeshWeaver.Blazor.Views/ViewsExtensions.cs:101.
The commit's rationale — "both prod portals already have the packs installed and enabled" — holds for a portal that already installed them. It does not hold for a mesh that has never talked to a registry. And preInstalled: true cannot cover it either: module install is restart-as-activation, so even pointing a fresh mesh at a registry leaves the first render as raw JSON.
Blast radius
Every fresh mesh: MeshWeaver.Education's disposable e2e, memex-local, and the first boot of any new deployment. Existing prod portals are unaffected — they installed the packs before the flip, which is exactly why this went unnoticed.
Concretely it is what turned MeshWeaver.Education main red: run 33106582408, all four Every course installs… shards plus The install actually installs (blocking) fail identically at e2e/bootstrap.setup.ts:147 waiting for getByText('package(s) available'). The catalog's own text is built from Controls.Stack/H1/Body (CatalogLayoutAreas.cs:262), every child of which is a NamedAreaControl — so that string can never appear, regardless of how many packages the source offers. Zero [bootstrap +Ns] marks were emitted; seeding itself succeeded (Node created at e2e-admin/Packages). It is the render that failed.
Attribution is clean: the signature appears 10× in the #221 merge run and 0× in the five preceding failed runs (whose failures were an unrelated per-course signature), and the packs are present in the previous pin 5a5564e5 and absent in 85a6de78.
Two ways to fix — this is a design call, not an obvious patch
A. Put the floor back in the image. Re-add the two MeshModuleClosure rows on the Plugins hosts and the two Modules:Assemblies + Modules:Required entries. Verified to build and lay out correctly (dotnet publish -c Release -r linux-x64 -p:CIRun=true, exit 0): modules/MeshWeaver.Blazor.Views/…dll plus .staticwebassets.endpoints.json and wwwroot/ (the #1724 static-asset lane intact), app root free of the DLL so MeshWeaverModulesGuardPublishAppRoot stays satisfied.
🚨 But this re-introduces precisely the shadow db552ffbf set out to remove — a baseline entry wins by name over a registry-landed upgrade (ModuleActivation.cs:497-540). It trades "the registry owns these packs" back for "the image ships a floor."
B. Keep the extraction, remove its cost. Make an ENABLED persisted entry override a same-named baseline in ComputeEffectiveModuleEntries (~5 lines + tests), then a baseline floor no longer shadows anything and A becomes safe. This preserves the Phase B2 end-state, but it changes a load-order contract several packs depend on.
The choice turns on whether Phase B2 means "registry only" or "registry wins", which is why this is filed rather than patched.
Whichever is chosen, Modules:Required should name the view packs, so this can never again be silent. On its own that only converts the failure from invisible to Degraded, named — the portal still comes up unusable — so it is a complement to A or B, not a fix.
Also worth correcting
MeshWeaver.Plugins/.github/workflows/ci.yml:203-209 still asserts the invariant this broke:
the source and the image copy move here together in the flip step, exactly so no deployment ever sees the pack in neither place
Three further places assert the image ships them: ViewsViewPackModuleAttribute.cs, BlazorViewRegistry.cs:57-59, Memex.Portal.Monolith.csproj:44-50. All are stale either way and should move with the fix.
Unrelated observation from the same run
The portal reports version=1.0.0 at runtime (host assembly from the Plugins repo) while the image tag is 3.0.0-rc8.ci.6141. The module floor gate reads a core assembly (ModulePlatformFloor.Resolve()), so minMeshVersion: 3.0.0-rc8 is still satisfied and nothing is broken by it — but the reported version is wrong and probably wants its own issue.
Ruled out as non-causal here: [CreateNode] REFUSED Plugins/_Policy (#1209, live work in #2534) — the same refusal shape appears for ApiToken and Admin/_Access/… in this run while both operations nonetheless succeeded, and the artifact shows a rendering fallback, not an access-denied page.
Activity
rbuergi commentedon Aug 27, 2026
Option A is written and verified, but deliberately not opened as a PR
A patch implementing option A exists on the branch
fix/view-packs-return-to-the-imageinMeshWeaver.Plugins(committed locally on the maintainer's machine, not pushed — commitbae5ed75). Four files, +90/−22:Memex.Portal.Distributed.csprojandMemex.Portal.Monolith.csproj— the twoMeshModuleClosurerows, in the same "host adds its own row after importing the targets" shape already used for Cosmos/Snowflake. Monolith's is gated onConfiguration != Debug, because Debug keeps its ships-the-bitsProjectReferences and a closure row plus an app-root copy is exactly whatMeshWeaverModulesGuardPublishAppRootrefuses.appsettings.json— the two DLLs underModules:AssembliesandModules:Required.Verified with a real publish rather than by reading (
dotnet publish -c Release -r linux-x64 -p:CIRun=true, exit 0, no error lines), checking for positive signals:modules/MeshWeaver.Blazor.Views/MeshWeaver.Blazor.Views.dll(408 KB) with its.staticwebassets.endpoints.jsonandwwwroot/— the Module static-asset provider: the one blocker keeping the Blazor view packs (Radzen, GoogleMaps, Analysis) out of modules/ and the Store #1724 static-asset lane intact — and the same for.Graph;appsettings.jsonparses withAssemblies: [Grpc, Views, Graph].Not verified: a booted portal emitting
[ModuleLoad]for either pack and painting a real catalog — that needs a running image, and MeshWeaver.Education's disposable mesh is the natural place to confirm it.It is left unopened on purpose: as written it re-introduces the baseline-shadows-registry behaviour that
db552ffbfremoved on purpose. If option B is preferred, that patch should be discarded rather than merged and then reverted.What's New: an installed pack is what loads (#2548)
Merge pull request #2558 from Systemorph/fix/2548-registry-wins