Repository navigation
Admin app: /Admin is the Admin app, personal settings are the person's own app, Inbox app for every user - #5807
Conversation
…n's own app; Inbox app for every user
/Admin rendered an empty page: no code registered the Admin partition root, so a fresh
database resolved it to a synthesized typeless placeholder (default hub configuration),
and long-lived databases carried a persisted Space row that also listed "Admin" among a
platform admin's workspaces.
- AdminAppNodeType: the AdminApp NodeType and the Admin root registered in code (part of
every image's closed type set, the control image included). /Admin opens the Settings
page of the Admin node; tabs at /Admin/Settings/{id}. Default node-management tabs are
hidden (HideSettingsTabs); Access Control / Effective Access of Admin stay.
- AddAdminAppTab / AdminOnlyTab: a tab yields only on the Admin hub and only after a
POSITIVE IsGlobalAdmin; relocated ids redirect from any other settings page
(RelocateSettingsTabsToAdminApp, honoured by SettingsLayoutArea and GlobalSettings).
- Moved into the app: Global Administration (was the admin's own user settings), Data
Sources (was the first global-settings tab every user landed on), Coupons, Instance
grants, Composition; and the Invitations / Inbox / Updates / Published / Privacy seeds
(NodeSettings + AdminOnly + NodeTypes:[AdminApp]). New: Overview (instance facts + About)
and Who am I.
- /{user}/Settings opens for its owner only (User nodes are public-read) and gains an
Account (who am I) tab.
- Inbox app: /{user}/Inbox renders the inbox legs for the owner; SeedInboxAppLogonAction
(run-once, create-if-absent) installs the tile for every user incl. the first admin.
- Doc: Doc/Architecture/AdminApp (inventory before -> after -> gate); references updated.
Tests: AdminAppTest (Admin is the app, not a Space; admin finds the surfaces; /Admin default
page; a non-admin reads nothing in Admin/_Access; own settings hold only personal tabs; other
people's settings refused; old admin link redirects), InboxAppTest (first admin + invited
user get the Inbox app through the real runner, idempotent), seed test updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The deployment report's adopted-framework inventory reads every NodeType node as a NodeTypeDefinition and refused the AdminApp declaration, which carried none (AnInstanceReportsWhatItRunsTest caught it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Test Results (shard 3)461 tests 461 ✅ 56s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
Test Results (shard 0) 1 files 1 suites 3m 14s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Authorization gates can remain stale after revocation, and Inbox rendering and seeding have unresolved issues.
Review effort: Lite
Findings: 1
Open (6)
Keep admin tab synchronized with permission revocations · New Recompute Who Am I rows after admin access changes · New Differentiate Running and Recent inbox queries · New Restore identity context before creating user-partition nodes · New Use localized labels for the Inbox launcher tile · New Use the authoritative node stream in the Admin test · New
What changed in this PR
This PR introduces a dedicated /Admin app, owner-only personal settings, and a per-user Inbox app.
Changes:
- Adds Admin app registration, gated administration tabs, redirects, and localization.
- Restricts personal settings to owners.
- Seeds and renders Inbox apps for users.
- Updates tests, documentation, and portal configuration.
| File | Reviewed change |
|---|---|
test/NodePagesWithoutProvenance.allow |
Updates provenance allowances. |
test/MeshWeaver.Graph.Test/InboxAppTest.cs |
Tests Inbox seeding and layout. |
test/MeshWeaver.Graph.Test/AdminAppTest.cs |
Tests Admin access and routing. |
test/Memex.Portal.Shared.Test/PlatformSettingsTabSeedTest.cs |
Validates relocated settings tabs. |
src/MeshWeaver.PluginCatalog/InstanceGrantAdminSettingsTab.cs |
Relocates instance grants. |
src/MeshWeaver.PluginCatalog/CouponAdminSettingsTab.cs |
Relocates Coupons. |
src/MeshWeaver.PluginCatalog/CompositionAdminSettingsTab.cs |
Relocates composition settings. |
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json |
Adds English strings. |
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json |
Adds German strings. |
src/MeshWeaver.Graph/UserActivityLayoutAreas.cs |
Adds Inbox app metadata. |
src/MeshWeaver.Graph/SettingsLayoutArea.cs |
Redirects relocated settings links. |
src/MeshWeaver.Graph/Logon/SeedInboxAppLogonAction.cs |
Seeds Inbox records at logon. |
src/MeshWeaver.Graph/InboxLayoutArea.cs |
Renders Inbox sections. |
src/MeshWeaver.Graph/GlobalSettingsLayoutArea.cs |
Moves Data Sources to Admin. |
src/MeshWeaver.Graph/Configuration/WhoAmISettingsTab.cs |
Adds identity and permission views. |
src/MeshWeaver.Graph/Configuration/UserNodeType.cs |
Adds owner-only settings and Inbox routing. |
src/MeshWeaver.Graph/Configuration/SettingsMenuItemsExtensions.cs |
Adds hidden settings-tab filtering. |
src/MeshWeaver.Graph/Configuration/LogonActionNodeType.cs |
Registers Inbox seeding. |
src/MeshWeaver.Graph/Configuration/GraphConfigurationExtensions.cs |
Registers the Admin app type. |
src/MeshWeaver.Graph/Configuration/GlobalSettingsMenuItemsExtensions.cs |
Relocates global settings. |
src/MeshWeaver.Graph/Configuration/GlobalAdministrationTab.cs |
Adds global administration. |
src/MeshWeaver.Graph/Configuration/AdminAppNodeType.cs |
Defines the Admin app and access gates. |
src/MeshWeaver.Documentation/Data/DataMesh/Spaces.md |
Updates navigation documentation. |
src/MeshWeaver.Documentation/Data/Architecture/PluginRegistry.md |
Updates admin navigation. |
src/MeshWeaver.Documentation/Data/Architecture/InvitationOnlyOnboarding.md |
Updates invitation navigation. |
src/MeshWeaver.Documentation/Data/Architecture/Instances.md |
Updates Instances navigation. |
src/MeshWeaver.Documentation/Data/Architecture/GitHubSync.md |
Updates partition navigation. |
src/MeshWeaver.Documentation/Data/Architecture/EnvironmentComposition.md |
Updates Composition navigation. |
src/MeshWeaver.Documentation/Data/Architecture/AdminApp.md |
Documents the application split. |
src/MeshWeaver.Documentation/Data/Architecture/AccessControl.md |
Updates admin-gate references. |
src/MeshWeaver.Documentation/Data/Architecture.md |
Adds the Admin app documentation link. |
memex/Memex.Portal.Shared/Settings/PlatformSettingsTabAreas.cs |
Moves platform tabs to Admin. |
memex/Memex.Portal.Shared/Settings/AdminAppOverviewTab.cs |
Adds the Admin overview tab. |
memex/Memex.Portal.Shared/MemexConfiguration.cs |
Registers Admin portal configuration. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return host.Hub.IsGlobalAdmin(viewerId) | ||
| .Where(isAdmin => isAdmin) | ||
| .Take(1) | ||
| .Select(_ => (IReadOnlyList<SettingsMenuItemDefinition>)[tab]) | ||
| .Timeout(AdminCheckBound) | ||
| .Catch<IReadOnlyList<SettingsMenuItemDefinition>, Exception>(_ => Observable.Return(none)) | ||
| .StartWith(none); |
There was a problem hiding this comment.
Fixed in 9a4b6ad. The gate is now AdminAppNodeType.LiveAdminVerdict — IsGlobalAdmin(viewer).Catch(→false).StartWith(false).DistinctUntilChanged(), no Where/Take(1)/Timeout — and AdminOnlyTab maps every emission: true → [tab], false → []. A grant revoked while the menu is open removes the tab. Pinned by AdminAppTest.TheAdminVerdict_FollowsARevokedGrant (grant written at runtime, verdict observed true, grant deleted, verdict observed false on the same subscription).
| var isAdmin = hub.IsGlobalAdmin(session.UserId) | ||
| .Where(a => a).Take(1) | ||
| .Timeout(TimeSpan.FromSeconds(5)) | ||
| .Catch<bool, Exception>(_ => Observable.Return(false)) | ||
| .StartWith(false) | ||
| .DistinctUntilChanged(); |
There was a problem hiding this comment.
Fixed in 9a4b6ad. The Who-am-I grid now combines the same live LiveAdminVerdict with the two permission folds, so the rows are recomputed on every emission and Yes turns back to No on revocation. Pinned by TheAdminVerdict_FollowsARevokedGrant (the stream) and TheWhoAmIRows_ShowTheCurrentVerdict (rows follow the verdict both ways).
| var seen = new HashSet<string>(StringComparer.Ordinal); | ||
| return InboxQueries.Resolve(legs, owner) | ||
| .OrderBy(leg => BandOrder.IndexOf(leg.Band) is var i and >= 0 ? i : BandOrder.Length) | ||
| .ThenBy(leg => leg.Band, StringComparer.Ordinal) | ||
| .ThenBy(leg => leg.Order) | ||
| .Where(leg => seen.Add(leg.Query)) |
There was a problem hiding this comment.
Fixed in 9a4b6ad, at the queries rather than the dedup key. Running is the ActivityStatus enum default and is OMITTED from stored content, so neither content.status:Running nor its negation can separate the bands (the trap OutboundEmailWatchQueryTest documents). Running now negates each stamped terminal state (-content.status:Succeeded -…Warning -…Failed -…Cancelled); Recent matches any of them (content.status:Succeeded|Warning|Failed|Cancelled). New tests: InboxAppTest.ThePage_ShowsThreeDistinctBands (NeedsYou, Running, Recent all render) and RunningAndRecent_SplitTheOwnersActivities (a running activity lands only in Running, a finished one only in Recent, on a real partition). InboxLegsAreAnchoredTest.NoLeg_UsesANamespaceAlternation now inspects the namespace:/path: anchor terms only — the content-field alternation filters inside the one partition.
| return Observable.Return(LogonActionOutcome.Nothing); | ||
|
|
||
| var node = UserActivityLayoutAreas.BuildAppRecord(ownerId, UserActivityLayoutAreas.InboxAppSpec(ownerId)); | ||
| return mesh.CreateNode(node) |
There was a problem hiding this comment.
Fixed in 9a4b6ad: the create is now access.RunAs(context.Identity, () => mesh.CreateNode(node)) at the write site, exactly as LogonActionRunner.Commit does. New InboxSeedWritesAsTheOwnerTest (no public-admin grants) sets a foreign ambient identity with no rights on the owner's partition and subscribes the action on a pool thread: without the RunAs it fails with Access denied: Create permission required for node 'inbox-runas-owner/_App/Inbox' (verified by reverting locally), with it the tile lands.
| ["Store"] = ("Store", "/static/NodeTypeIcons/shopping-bag.svg"), | ||
| ["Doc"] = ("Documentation", "/static/NodeTypeIcons/book.svg"), | ||
| ["~/" + ChatArea] = ("Threads", ThreadsIcon), | ||
| ["~/" + InboxLayoutArea.AreaName] = ("Inbox", InboxIcon), |
There was a problem hiding this comment.
Fixed in 9a4b6ad + Plugins#2450 (c358d416a). App gains LabelKey; the Inbox record carries inbox.title, and the launcher (MeshSearchView.TileLabel, Plugins) resolves it in the VIEWER's language, falling back to the stored name. The stored Name is seeded in the owner's language (context.Identity.Locale) for any surface that paints the name alone. Tests: InboxAppTest.TheTile_IsLocalized (German owner → Posteingang, key stored) and Plugins AppTileLabelTest (German viewer sees Posteingang from typed and raw-JSON content; no/unknown key keeps the stored name).
| var nodes = await QueryAs(PlatformAdmin, $"path:{AdminAppNodeType.Path}", TestContext.Current.CancellationToken); | ||
|
|
||
| var admin = nodes.Should().ContainSingle(n => n.Path == AdminAppNodeType.Path).Subject; |
There was a problem hiding this comment.
Fixed in 9a4b6ad: TheAdminRoot_IsTheAdminApp reads Mesh.GetWorkspace().GetMeshNodeStream(AdminAppNodeType.Path) as the platform admin — the authoritative read of the one known path — instead of the query index.
Test Results (shard 1)1 693 tests 1 693 ✅ 4m 47s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
Test Results (shard 4) 3 files 3 suites 6m 50s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)764 tests 573 ✅ 7m 8s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 5 files 5 suites 14m 49s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
Test Results 17 files 17 suites 37m 46s ⏱️ Results for commit b34d29c. ♻️ This comment has been updated with latest results. |
…at the seed's write, localized Inbox tile - AdminOnlyTab and the Who-am-I grid observe the admin verdict LIVE (AdminAppNodeType.LiveAdminVerdict): a revoked grant removes the tab and turns Yes to No; no .Where(true).Take(1) one-shot on a stream feeding a live view. - InboxQueries: Running = no stamped terminal status, Recent = any stamped terminal status (Running is the omitted enum default, so it cannot be matched either way); all three bands render. The anchor guard now checks namespace:/path: terms only. - SeedInboxAppLogonAction: the create runs under access.RunAs(context.Identity, ...) at the write site, as LogonActionRunner.Commit does. InboxSeedWritesAsTheOwnerTest subscribes it on a pool thread under a foreign ambient identity: refused without the fix, green with it. - App.LabelKey: the Inbox record carries inbox.title (resolved per viewer by the launcher, Plugins) and its stored name is seeded in the owner's language. - AdminAppTest reads the Admin node through GetMeshNodeStream, not the query index; new TheAdminVerdict_FollowsARevokedGrant and TheWhoAmIRows_ShowTheCurrentVerdict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Adopting this branch to resolve the merge conflict with main for the maintainer (branch idle since 13:20Z, PR is DIRTY). I will merge origin/main into feat/admin-app, preserve both sides' intent, build the touched projects with -c Release -warnaserror, and push. No change to the PR's scope. |
…'s additions) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pp tab too The merge of main unioned the localization keys but left main's new admin-only "Control lane" seed on the global settings page. The merged seed test lists ControlLane among the admin tab ids (main) and requires every admin tab to be an Admin-app tab (this branch), so PlatformSettingsTabSeedTest failed 3 of 6 at bac07bd. Move it like the other Administration seeds: NodeSettings context, gated AdminOnly + NodeTypes [AdminApp], and listed in AdminAppTabIds so the old /GlobalSettings/ControlLane link redirects to /Admin/Settings/ControlLane. AdminApp.md's inventory lists it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merge with main is on the branch (bac07bd, the concurrent merge that landed while I was resolving; mine resolved the same catalog conflict identically: union of both sides' keys, en/de key sets equal, no duplicates). It missed one semantic conflict: main's #5791 added an admin-only Control lane seed to the global settings page, while this PR moves every Administration tab into the Admin app. The merged seed test lists ControlLane as an admin tab, so |



Why
Maintainer, on a freshly installed control instance: "https://control.systemorph.com/Admin shows an empty screen. should this be the admin app? … don't make it of type Space … link it to some Admin app which is installed by default and shows me all the about, who I am, all the settings", then "all which should be seen only by global admin ⇒ move to this app. remove from settings", "do same for user settings" and "every new user should get Inbox app installed".
What
/Adminrendered before, measured from code: no code registered theAdminpartition root. On a fresh database (the new control instance) there is no row, soPathResolutionService.SynthesizePartitionRootresolved a TYPELESS placeholder whose hub activated on the default configuration — an empty page. On long-lived databases the row exists as aSpace(read on memex.systemorph.com and memex.meshweaver.cloud:nodeType: Space, no content), mirrored into theauthSpaces catalog — so "Admin" was listed among a platform admin's workspaces and rendered as an empty Space.What it renders now
/Adminis the Admin app —AdminAppNodeTyperegisters theAdminAppNodeType and theAdminroot in code (so it is in every image's closed type set, the control image included); the app is the Settings page of the Admin node, tabs at/Admin/Settings/{id}./{user}/Settingsis the person's own settings app, for its owner only. Full inventory:Doc/Architecture/AdminApp(in this PR)./Adminwhoamionly/Admin/Settings/WhoAmI,/{user}/Settings/AccountAdmin/_Access, + Add Admin)/{user}/Settings/GlobalAdmin/Admin/Settings/GlobalAdmin/_Setting/GlobalSettings/DataSources(first tab for EVERY signed-in user)/Admin/Settings/DataSourcesAdmin/Admin/Settings/AccessControl/{user}/Settings/{id}/Admin/Settings/{id}Admin/UpdatePolicy), Published, Privacy/_Setting/GlobalSettings/{id}/Admin/Settings/{id}/Admin/Settings/{id}/{user}/Settings(any viewer could open it: User nodes are public-read)/{user}/Settings, owner only/_Setting/{user}/Inbox, tile{user}/_App/Inboxseeded by run-onceseed-inbox-appfor every user (first admin included)A non-admin opening
/Adminis refused by the Admin partition itself (unchanged — nothing widens a read onAdmin/_Access), and every admin tab additionally waits for a positiveIsGlobalAdmin. Old admin links redirect into the app (RelocateSettingsTabsToAdminApp, honoured by the per-node and the global settings page).Tests
AdminAppTest(new): the Admin root isAdminApp(fails on main: no node exists); the workspace listing of a platform admin does not list Admin (control: a real Space is listed); the platform admin finds Global Administration / Data Sources / Who am I / Access Control and no node-management tabs;/Adminwith no area lands on the app; a non-admin reads NOTHING of a persistedAdmin/_Accessgrant (control: the admin does); a non-admin opening/Adminsees no administration; own settings hold only personal tabs for an ordinary user AND a platform admin; someone else's settings are refused;/{admin}/Settings/GlobalAdminredirects to/Admin/Settings/GlobalAdmin.InboxAppTest(new): a freshly created first admin and an invited user both get{user}/_App/Inboxthrough the realLogonActionRunner, run twice; the action is idempotent, run-once, stable key; the page anchors every leg on the owner, band order, each query once.PlatformSettingsTabSeedTestupdated to the Admin-app lane;AnInstanceReportsWhatItRunsTestcaught a declaration without aNodeTypeDefinition(fixed in this PR).dotnet build MeshWeaver.slnx -c Release -warnaserror0 errors / 0 warnings;MeshWeaver.Graph.Test2166/2166,Memex.Portal.Shared.Test2054 (the 2 reds were the missing definition, now green),MeshWeaver.Documentation.Test649/649,LocalizationTest66/66.AddMeshNodes(static provider) are not row-filtered — an ordinary user lists another user's static_Accessgrant — so the access test asserts on a runtime-written grant, as production holds.How it reaches an instance
Compiled platform code → an image roll (hop C). The companion MeshWeaver.Plugins PR carries migration V59 (
DbVersion.Latest59) that retypes the persistedAdminSpace row toAdminAppand drops itsauthmirror row; the roll runs it before the new pods serve. The control image is the sameMemex.Portal.Distributedhost withMesh:ClosedTypeSet=true;AdminAppis registered in code byAddGraph, so it is in the closed set — no package install. After the roll, recycleAdmin(a live activation bound to the old Space/typeless configuration serves it until aDisposeRequestreaches it).Pairs-with: none — no public type or member leaves
src/(UserNodeType's global-admin tab methods were private;GlobalSettingsLayoutArea.BuildDataSourcesTabis internal). The MeshWeaver.Plugins PR consumes the newAdminAppNodeTypeAPI and lands AFTER this one.Implementers: none — no interface gains a member.
Mirror-sync:
npm run sync:i18n -- --ref <merged core sha>in MeshWeaver.Pluginsclients/react, handed to the companion Plugins PR (41 new keys:adminApp.*,whoAmI.*,inbox.*,globalAdmin.*,settings.whoAmI/account/ownSettingsOnly/dataSourcesIntro/mySettings; no existing value changed).🤖 Generated with Claude Code