Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f055086
Kernel: a control result is rendered, not also dumped as text
sierragolflima Aug 6, 2026
5397100
Merge remote-tracking branch 'origin/main' into fix/kernel-uicontrol-…
sierragolflima Aug 6, 2026
52b6429
Fix CS9107: use the base's Output, not the captured constructor param…
sierragolflima Aug 6, 2026
1682fe6
Merge branch 'main' into fix/kernel-uicontrol-tostring-log
sierragolflima Aug 6, 2026
b1caba5
Clearer MeshNode menu labels + localized AI/GitHub tooltips
sierragolflima Aug 6, 2026
92db732
Merge remote-tracking branch 'origin/main' into fix/meshnode-menu-labels
sierragolflima Aug 6, 2026
8e8db66
Merge remote-tracking branch 'origin/docs/release-pipeline' into inte…
rbuergi Aug 6, 2026
5ef8735
Merge remote-tracking branch 'origin/fix/kernel-uicontrol-tostring-lo…
rbuergi Aug 6, 2026
cb1cfcc
Merge remote-tracking branch 'origin/fix/doc-example-layout-areas' in…
rbuergi Aug 6, 2026
e9f80ad
Merge remote-tracking branch 'origin/feat/cd-ships-portal-next' into …
rbuergi Aug 6, 2026
7eca239
Merge remote-tracking branch 'origin/docs/version-authoring-and-merge…
rbuergi Aug 6, 2026
d2c62e4
Merge remote-tracking branch 'origin/fix/button-style' into integrati…
rbuergi Aug 6, 2026
4c4939e
Merge remote-tracking branch 'origin/fix/flake-sweep' into integratio…
rbuergi Aug 6, 2026
4975b78
Merge remote-tracking branch 'origin/fix/rollback-refused-by-monotoni…
rbuergi Aug 6, 2026
dd27537
Merge remote-tracking branch 'origin/fix/meshnode-menu-labels' into i…
rbuergi Aug 6, 2026
894fb84
Merge remote-tracking branch 'origin/fix/workspace-dispose-sync-react…
rbuergi Aug 6, 2026
dcf0458
Merge branch 'main' into fix/kernel-uicontrol-tostring-log
rbuergi Aug 6, 2026
cf0536c
Merge branch 'main' into fix/meshnode-menu-labels
rbuergi Aug 6, 2026
adeaf6a
Merge remote-tracking branch 'origin/docs/release-pipeline' into inte…
rbuergi Aug 6, 2026
27c3f06
Merge remote-tracking branch 'origin/fix/kernel-uicontrol-tostring-lo…
rbuergi Aug 6, 2026
df05635
Merge remote-tracking branch 'origin/fix/doc-example-layout-areas' in…
rbuergi Aug 6, 2026
30858e9
Merge remote-tracking branch 'origin/feat/cd-ships-portal-next' into …
rbuergi Aug 6, 2026
69df3f3
Merge remote-tracking branch 'origin/docs/version-authoring-and-merge…
rbuergi Aug 6, 2026
991dee0
Merge remote-tracking branch 'origin/fix/button-style' into integrati…
rbuergi Aug 6, 2026
24b4593
Merge remote-tracking branch 'origin/fix/flake-sweep' into integratio…
rbuergi Aug 6, 2026
c00076d
Merge remote-tracking branch 'origin/fix/rollback-refused-by-monotoni…
rbuergi Aug 6, 2026
65f1ac5
Merge remote-tracking branch 'origin/fix/meshnode-menu-labels' into i…
rbuergi Aug 6, 2026
92960fd
Merge remote-tracking branch 'origin/fix/workspace-dispose-sync-react…
rbuergi Aug 6, 2026
6e8085f
ci: retrigger after Actions outage (no code change)
rbuergi Aug 6, 2026
c9ce2d7
Merge branch 'main' into integration/all-open-prs
rbuergi Aug 7, 2026
4c220fe
Merge branch 'main' into fix/meshnode-menu-labels
rbuergi Aug 7, 2026
fc253b4
Merge branch 'main' into fix/meshnode-menu-labels
rbuergi Aug 7, 2026
7bbc9d8
Merge branch 'main' into integration/all-open-prs
rbuergi Aug 7, 2026
351cff5
Merge main (CI image preflight + assertion diagnostics)
rbuergi Aug 7, 2026
b3ab102
Update MenuAccessControlTest for the labels this PR renames
rbuergi Aug 7, 2026
8e9316b
Merge main
rbuergi Aug 7, 2026
0f60891
Merge fix/meshnode-menu-labels (label rename + its test update)
rbuergi Aug 7, 2026
dc91397
ScriptExecutionInUserHomeTest encoded the logging contract this PR ch…
rbuergi Aug 7, 2026
4e04ca6
DataChangeLogTest raced hub startup on every write
rbuergi Aug 7, 2026
ac671b2
Merge fix/meshnode-menu-labels (DataChangeLogTest startup-race fix)
rbuergi Aug 7, 2026
6b898ff
Align the cold-start node read in GetCreatableTypesAt with its own si…
rbuergi Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/MeshWeaver.Blazor.Portal/Layout/PortalLayoutBase.razor
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
}
</FluentMenu>
<FluentButton Id="ai-menu-anchor" Appearance="Appearance.Stealth" BackgroundColor="transparent"
Title="AI" aria-label="@Access.Localize("menu.ai")"
Title="@Access.Localize("menu.ai")" aria-label="@Access.Localize("menu.ai")"
OnClick="@ToggleAiMenu">
<FluentIcon Value="@(new Icons.Regular.Size20.Sparkle())" Color="Color.FillInverse" />
</FluentButton>
Expand All @@ -191,7 +191,7 @@
}
</FluentMenu>
<FluentButton Id="github-menu-anchor" Appearance="Appearance.Stealth" BackgroundColor="transparent"
Title="GitHub" aria-label="@Access.Localize("menu.github")"
Title="@Access.Localize("menu.github")" aria-label="@Access.Localize("menu.github")"
OnClick="@ToggleGitHubMenu">
<FluentIcon Value="@(new Icons.Regular.Size20.BranchFork())" Color="Color.FillInverse" />
</FluentButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
Name: Clearer MeshNode menu labels
Category: What's New
Description: The mesh menu spells out Create / Import / Export MeshNode, and the AI and GitHub menu tooltips now read consistently.
Icon: Sparkle
---

# Clearer MeshNode menu labels

The mesh menu next to the search bar now labels its actions **Create MeshNode**, **Import MeshNode**, and **Export MeshNode**, so it is clear they operate on mesh nodes rather than uploading or downloading arbitrary files.

The top-bar menu buttons also read consistently on hover: the AI button now shows "AI menu" and the GitHub button "GitHub menu", matching the Node and Mesh buttons. All labels are available in English and German.
11 changes: 10 additions & 1 deletion src/MeshWeaver.Kernel.Hub/KernelExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,16 @@ private void ClearCancellationIf(CancellationTokenSource cts)
if (returnValue is not null)
{
UpdateView(viewId, returnValue);
scriptOutputLogger.LogInformation("{Value}", returnValue.ToString() ?? "");
// A CONTROL is already rendered by UpdateView above — logging it as well
// prints its record ToString() into the cell's output pane beside the very
// thing it describes ("StackControl { Id = , Style = , Skins = System.
// Collections.Immutable.ImmutableList`1[...] }"). Observed on
// RiskTransfer/01-GrossToNet under the rendered loss-book grid.
//
// Everything else still logs: for `1 + 1` or a string, that line IS the
// cell's output and the only thing the learner sees.
if (returnValue is not IUiControl)
scriptOutputLogger.LogInformation("{Value}", returnValue.ToString() ?? "");
}
return returnValue;
}));
Expand Down
6 changes: 3 additions & 3 deletions src/MeshWeaver.Messaging.Hub/Localization/strings.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@
"menu.versions": "Versionen",
"menu.recycle": "In den Papierkorb",
"menu.threads": "Threads",
"menu.create": "Erstellen",
"menu.import": "Importieren",
"menu.export": "Exportieren",
"menu.create": "MeshNode erstellen",
"menu.import": "MeshNode importieren",
"menu.export": "MeshNode exportieren",
"menu.stopSync": "Synchronisierung stoppen",
"menu.resumeSync": "Synchronisierung fortsetzen",
"menu.synchronizations": "Synchronisierungen",
Expand Down
6 changes: 3 additions & 3 deletions src/MeshWeaver.Messaging.Hub/Localization/strings.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@
"menu.versions": "Versions",
"menu.recycle": "Recycle",
"menu.threads": "Threads",
"menu.create": "Create",
"menu.import": "Import",
"menu.export": "Export",
"menu.create": "Create MeshNode",
"menu.import": "Import MeshNode",
"menu.export": "Export MeshNode",
"menu.stopSync": "Stop synchronization",
"menu.resumeSync": "Resume synchronization",
"menu.synchronizations": "Synchronizations",
Expand Down
16 changes: 12 additions & 4 deletions test/MeshWeaver.AI.Test/ScriptExecutionInUserHomeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ public async Task Run_FireworksScript_StreamsProgressAndReturnsHtml()
final.Messages.Select(m => m.Message).Should()
.Contain(m => m.Contains("Loading fuse")).And
.Contain(m => m.Contains("Boom!")).And
.Contain(m => m.Contains("🎆"), "fireworks return value lands as the terminal message");
// 🚨 The fireworks return value is a CONTROL, and a control is rendered by UpdateView
// — KernelExecutor deliberately no longer ALSO logs its ToString(), which would print
// "HtmlControl { Id = , Style = … }" into the output pane beside the very thing it
// describes. So the log carries the 4 progress lines and not the return value.
.NotContain(m => m.Contains("HtmlControl"),
"a rendered control must not also be dumped as its record ToString()");
}

/// <summary>
Expand Down Expand Up @@ -120,7 +125,9 @@ public async Task Run_StreamsProgressTimely_NotBuffered()
.Select(change => (Count: (change?.Content as ActivityLog)?.Messages.Count ?? 0,
ElapsedMs: sw.ElapsedMilliseconds))
.DistinctUntilChanged(o => o.Count)
.TakeUntil(o => o.Count >= 5) // 4 progress + 1 fireworks return-value
// 4 progress lines. The fireworks RETURN VALUE is a control, which UpdateView renders
// and KernelExecutor deliberately does not also log — so it never becomes a message.
.TakeUntil(o => o.Count >= 4)
.ToList()
.Should().Within(30.Seconds()).Emit();

Expand All @@ -129,8 +136,9 @@ public async Task Run_StreamsProgressTimely_NotBuffered()
+ "single-tick delivery would mean the executor blocked the activity hub. Observed: ["
+ string.Join(", ", observations.Select(o => $"{o.Count}@{o.ElapsedMs}ms")) + "]");

observations.Last().Count.Should().Be(5,
"terminal snapshot should contain all 4 progress messages + fireworks return value");
observations.Last().Count.Should().Be(4,
"terminal snapshot should contain the 4 progress messages; the fireworks return value "
+ "is a control, rendered by UpdateView rather than logged as its record ToString()");
}

/// <summary>
Expand Down
28 changes: 26 additions & 2 deletions test/MeshWeaver.Data.Test/DataChangeLogTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,31 @@ protected override MessageHubConfiguration ConfigureHost(MessageHubConfiguration
.WithInitialData(_ => Observable.Return<IEnumerable<ChangeLogRecord>>(
[new ChangeLogRecord("1", "First")])))));

/// <summary>
/// Returns the workspace only once its data source's stream hub has actually STARTED.
///
/// <para>🚨 <c>WorkspaceOperations.UpdateStream</c> hard-throws
/// <c>"Data source … is not initialized."</c> when a change arrives before
/// <c>stream.Hub.Started</c> has completed. Writing straight after
/// <c>GetHost().GetWorkspace()</c> is therefore a race against hub startup: it wins on a
/// developer machine (the whole class runs in ~370 ms) and loses on a loaded CI runner,
/// where it surfaced as a one-test failure that looked like an unrelated flake.</para>
///
/// <para>The seeded record <c>"1"</c> is the readiness signal — observing it proves the
/// source is up. We wait on that ACTUAL condition rather than sleeping (WritingTests.md).</para>
/// </summary>
private async Task<IWorkspace> GetStartedWorkspace()
{
var workspace = GetHost().GetWorkspace();
await workspace.GetObservable<ChangeLogRecord>()
.Should().Within(10.Seconds()).Match(x => x.Any(r => r.Id == "1"));
return workspace;
}

[Fact]
public async Task Change_ReportsOnce_ThenCompletes()
{
var workspace = GetHost().GetWorkspace();
var workspace = await GetStartedWorkspace();

var notifications = await workspace
.RequestChange(DataChangeRequest.Update([new ChangeLogRecord("2", "Second")]))
Expand All @@ -66,7 +87,7 @@ public async Task Change_ReportsOnce_ThenCompletes()
[Fact]
public async Task Change_WhenValidationFails_ReportsTheFailure_AndDoesNotWrite()
{
var workspace = GetHost().GetWorkspace();
var workspace = await GetStartedWorkspace();

var log = await workspace
.RequestChange(DataChangeRequest.Update([new ChangeLogRecord("3", null)]))
Expand All @@ -86,6 +107,9 @@ public async Task Change_WhenValidationFails_ReportsTheFailure_AndDoesNotWrite()
public async Task DataChangeRequest_DoesNotHostAnActivityHub()
{
var host = GetHost();
// Same startup race as above: the change below reaches the very same UpdateStream guard,
// just via the client hub instead of directly.
await GetStartedWorkspace();
var hostedHubs = host.ServiceProvider.GetRequiredService<HostedHubsCollection>();
int ActivityHubCount() => hostedHubs.Hubs.Count(h => h.Address.Type == AddressExtensions.ActivityType);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,20 @@ private async Task<IReadOnlyList<CreatableTypeInfo>> GetCreatableTypesAt(string
MeshNode? parent = null;
if (!string.IsNullOrEmpty(nodePath))
{
// FIRST read of a mesh-backed node: this waits for the mesh to hydrate the partition,
// not for a quick in-memory lookup. 5 s was an unjustified outlier — the sibling call
// below, part of the same logical operation in this same helper, already allows 60 s,
// and the method timeout is 60 s. On a cold 2-core CI runner the read had simply not
// produced its first emission yet, and the assertion reported "emitted nothing at
// all" — which reads as a wedge rather than as a bound below legitimate hydration.
//
// A bound ALIGNMENT, not cover for a hang: the read does complete (green locally in
// ~1 s), and a real wedge still fails — at the same 60 s the rest of the operation
// already uses, instead of 12x sooner for no stated reason.
parent = await workspace.GetMeshNodeStream(nodePath)
.Take(1)
.Catch<MeshNode?, Exception>(_ => Observable.Return<MeshNode?>(null))
.Should().Within(TimeSpan.FromSeconds(5)).Emit();
.Should().Within(60.Seconds()).Emit();
}
return await CreatableTypesProvider.GetCreatableTypes(nodePath, parent)
.Should().Within(20.Seconds()).Emit();
Expand Down Expand Up @@ -949,10 +959,20 @@ private async Task<IReadOnlyList<CreatableTypeInfo>> GetCreatableTypesAt(string
MeshNode? parent = null;
if (!string.IsNullOrEmpty(nodePath))
{
// FIRST read of a mesh-backed node: this waits for the mesh to hydrate the partition,
// not for a quick in-memory lookup. 5 s was an unjustified outlier — the sibling call
// below, part of the same logical operation in this same helper, already allows 60 s,
// and the method timeout is 60 s. On a cold 2-core CI runner the read had simply not
// produced its first emission yet, and the assertion reported "emitted nothing at
// all" — which reads as a wedge rather than as a bound below legitimate hydration.
//
// A bound ALIGNMENT, not cover for a hang: the read does complete (green locally in
// ~1 s), and a real wedge still fails — at the same 60 s the rest of the operation
// already uses, instead of 12x sooner for no stated reason.
parent = await workspace.GetMeshNodeStream(nodePath)
.Take(1)
.Catch<MeshNode?, Exception>(_ => Observable.Return<MeshNode?>(null))
.Should().Within(TimeSpan.FromSeconds(5)).Emit();
.Should().Within(60.Seconds()).Emit();
}
return await CreatableTypesProvider.GetCreatableTypes(nodePath, parent)
.Should().Within(60.Seconds()).Emit();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using MeshWeaver.Data;
using MeshWeaver.Hosting.Monolith.TestBase;
using MeshWeaver.Kernel;
using MeshWeaver.Layout;
using MeshWeaver.Mesh;
using MeshWeaver.Mesh.Services;
using MeshWeaver.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace MeshWeaver.Hosting.Monolith.Test;

/// <summary>
/// A cell whose last expression is a CONTROL renders that control; it must not ALSO dump the
/// control's <c>ToString()</c> into the cell's output.
///
/// <para><c>KernelExecutor</c> hands a non-null return value to <c>UpdateView</c>, which renders
/// it. Logging the same object as well printed its record shape beside the rendered thing —
/// "StackControl { Id = , Style = , Skins = System.Collections.Immutable.ImmutableList`1[…] }" —
/// observed under the loss-book grid on <c>RiskTransfer/01-GrossToNet</c>.</para>
///
/// <para>The log line is still the RIGHT behaviour for every other result: for <c>1 + 1</c> it is
/// the only output the learner sees. Both directions are asserted, because suppressing the control
/// dump by silencing the logger outright would be the easy wrong fix.</para>
///
/// <para>🚨 Each cell gets its OWN activity node, and the assertion waits for that activity to
/// reach a TERMINAL status before reading. The activity log is REPLACED per submission, not
/// accumulated: running two cells against one activity and reading afterwards inspects only the
/// second cell's log, so the assertion about the first passes while never seeing its output.</para>
/// </summary>
public class KernelControlResultNotLoggedTest(ITestOutputHelper output) : MonolithMeshTestBase(output)
{
protected override MessageHubConfiguration ConfigureClient(MessageHubConfiguration configuration)
=> base.ConfigureClient(configuration).AddLayoutClient();

[Fact(Timeout = 180_000)]
public async Task ControlResult_IsRendered_ButNotDumpedIntoTheOutput()
{
var controlLog = await RunCellAsync(
"ctrl",
"""
MeshWeaver.Layout.Controls.Stack
.WithView(MeshWeaver.Layout.Controls.Markdown("rendered, not dumped"))
""");

var scalarLog = await RunCellAsync(
"scalar",
"""
$"scalar-result-{40 + 2}-marker"
""");

Output.WriteLine($"control cell log ({controlLog.Length}):");
foreach (var m in controlLog) Output.WriteLine($" {(m.Length > 160 ? m[..160] + "…" : m)}");
Output.WriteLine($"scalar cell log ({scalarLog.Length}):");
foreach (var m in scalarLog) Output.WriteLine($" {(m.Length > 160 ? m[..160] + "…" : m)}");

// The scalar direction FIRST: it proves result-logging reached this log at all. Without it,
// an empty log would satisfy the control assertion for entirely the wrong reason.
scalarLog.Should().Contain(m => m.Contains("scalar-result-42-marker"),
"a non-control result is still logged — for a scalar cell that line IS the output");

controlLog.Should().NotContain(m => m.Contains("StackControl {"),
"a control is rendered by UpdateView — logging its ToString() as well prints the "
+ "record's field list into the output pane beside the rendered control");
}

/// <summary>Runs one cell on a fresh activity and returns that activity's settled messages.</summary>
private async Task<string[]> RunCellAsync(string label, string code)
{
var meshService = Mesh.ServiceProvider.GetRequiredService<IMeshService>();
var id = $"{label}-{Guid.NewGuid():N}";
const string ownerPath = "rbuergi";
var activityNamespace = $"{ownerPath}/_Activity";
var node = new MeshNode(id, activityNamespace)
{
Name = $"Control-result logging probe ({label})",
NodeType = "Activity",
MainNode = ownerPath,
State = MeshNodeState.Active,
Content = new ActivityLog("KernelExecution") { Status = ActivityStatus.Running }
};
await meshService.CreateNode(node).Should().Within(60.Seconds()).Emit();

var client = GetClient();
var address = new Address($"{activityNamespace}/{id}");

// Settle on the TERMINAL status — the result line is written after the script returns, so
// waiting on any earlier marker reads the log before the value under test is in it.
var settled = client.GetWorkspace()
.GetMeshNodeStream(address.Path)
.Select(n => n?.Content as ActivityLog)
.Where(l => l is not null && l!.Status != ActivityStatus.Running)
.Select(l => l!.Messages.Select(m => m.Message).ToArray())
.FirstAsync()
.ToTask();

client.Post(new SubmitCodeRequest(code), o => o.WithTarget(address));
return await settled.WaitAsync(TimeSpan.FromSeconds(120));
}
}
10 changes: 5 additions & 5 deletions test/MeshWeaver.Security.Test/MenuAccessControlTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ public async Task Menu_Editor_ShowsCreateItems()
// fix for the old flake, where the menu was read before the Editor role propagated.
var expected = new[]
{
"Edit", "Create", "Copy", "Import", "Files", "Data", "Export", "Versions", "Pin", "Recycle",
"Edit", "Create MeshNode", "Copy", "Import MeshNode", "Files", "Data", "Export MeshNode", "Versions", "Pin", "Recycle",
"Stop synchronization"
};
var items = await FetchAllMenuItems(client, nodeAddress, LabelsAre(expected));
Expand Down Expand Up @@ -241,7 +241,7 @@ public async Task Menu_Admin_ShowsAllItems()

var expected = new[]
{
"Edit", "Create", "Copy", "Move", "Import", "Files", "Data", "Export", "Versions", "Delete", "Pin", "Recycle",
"Edit", "Create MeshNode", "Copy", "Move", "Import MeshNode", "Files", "Data", "Export MeshNode", "Versions", "Delete", "Pin", "Recycle",
"Stop synchronization"
};
var items = await FetchAllMenuItems(client, nodeAddress, LabelsAre(expected));
Expand Down Expand Up @@ -271,7 +271,7 @@ public async Task Menu_ItemsAreSortedByOrder()
var nodeItems = await FetchMenuItems(client, nodeAddress, NodeMenuItemsExtensions.NodeMenuContext,
items => items.Any(i => i.Label == "Delete"));
var meshItems = await FetchMenuItems(client, nodeAddress, NodeMenuItemsExtensions.MeshMenuContext,
items => items.Any(i => i.Label == "Create"));
items => items.Any(i => i.Label == "Create MeshNode"));

nodeItems.Should().BeInAscendingOrder(i => i.Order, "Node menu items should be sorted by Order");
meshItems.Should().BeInAscendingOrder(i => i.Order, "Mesh menu items should be sorted by Order");
Expand All @@ -291,9 +291,9 @@ public async Task Menu_ImportAreaIsImportMeshNodes()

// Import lives in the Mesh menu — wait until it appears (Editor role propagated).
var meshItems = await FetchMenuItems(client, nodeAddress, NodeMenuItemsExtensions.MeshMenuContext,
items => items.Any(i => i.Label == "Import"));
items => items.Any(i => i.Label == "Import MeshNode"));

var importItem = meshItems.FirstOrDefault(i => i.Label == "Import");
var importItem = meshItems.FirstOrDefault(i => i.Label == "Import MeshNode");
importItem.Should().NotBeNull("Import menu item should exist in the Mesh menu for Editor");
importItem!.Area.Should().Be(MeshNodeLayoutAreas.ImportMeshNodesArea,
"Import should navigate to ImportMeshNodes area, not $Import");
Expand Down
Loading