---
title: "Explorer source"
url: "https://nsta1.github.io/Orleans.Lattice/samples/Explorer/source.html"
source: "https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/Explorer"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
---
# Explorer source

Part of [Explorer sample](README.md).

The source of the [Explorer](https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/Explorer) sample.

## Program.cs

````csharp
using System.Diagnostics;
using Orleans.Lattice.Samples.Explorer;

// Orleans.Lattice.Explorer sample: one process that shows every area of the
// Explorer console with live data, with no cloud dependency.
//
// By default it runs a two-region estate: the east and west regions are two
// single-silo Orleans clusters in this process, each serving every control
// plane the Explorer has an area for on its own gRPC endpoint, replicating with
// each other over loopback gRPC, and sharing one in-memory backup sink. East
// also serves the console, which connects to east (or to west, with
// --explorer-region west). Tenancy is on, with two seeded tenants. --minimal
// keeps the single-region experience with no tenancy and no peer.
//
// The console signs in automatically as the bootstrap administrator, so every
// administrator-gated area lights up. Telemetry is the one area that stays
// hidden: it reads a Prometheus-compatible metrics backend, which this sample
// does not run. README.md walks each area.

if (!ExplorerSampleOptions.TryParse(args, Environment.GetEnvironmentVariable, out var options, out var error))
{
    Console.WriteLine(error);
    return 2;
}

var clock = Stopwatch.StartNew();
Console.WriteLine(options.Minimal ? "Starting one region..." : "Starting the east and west regions...");

await using var sample = ExplorerSample.Create(options);
await sample.StartAsync();
SampleBanner.Write(Console.Out, sample, clock.Elapsed);

sample.PeerLink.Changed += paused => Console.WriteLine(paused
    ? "Peer link PAUSED: replication between east and west is refused. Watch Replication go Lagging, then Stalled."
    : "Peer link RESUMED: the regions catch up.");

using var stop = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    stop.Cancel();
};

if (sample.West is not null)
{
    _ = Task.Run(() => ReadPeerToggles(sample.PeerLink, stop.Token));
}

try
{
    await Task.Delay(Timeout.Infinite, stop.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Stopping...");
}

return 0;

// P (or a line starting with p, when input is redirected) pauses or resumes the
// link between the regions.
static void ReadPeerToggles(PeerLink link, CancellationToken stop)
{
    while (!stop.IsCancellationRequested)
    {
        if (Console.IsInputRedirected)
        {
            var line = Console.In.ReadLine();
            if (line is null)
            {
                return;
            }

            if (line.Trim().StartsWith('p') || line.Trim().StartsWith('P'))
            {
                link.Toggle();
            }
        }
        else if (Console.ReadKey(intercept: true).Key == ConsoleKey.P)
        {
            link.Toggle();
        }
    }
}
````

## Apps/TaskBoard/src/manifest.json

````json
{
  "identity": {
    "slug": "task-board",
    "version": "1.0.0",
    "provenance": {
      "source": "in-image",
      "publisher": "Orleans.Lattice samples",
      "reference": "embedded:Orleans.Lattice.Samples.Explorer.TaskBoard.manifest.json"
    }
  },
  "presentation": {
    "displayName": "Task board",
    "summary": "A three-column task board whose cards live as JSON values in the app's own tree.",
    "description": "Task board is the Explorer's pilot app with an untrusted UI.\nIt lists, adds, moves and deletes task cards stored under tasks/{id} in its one tree, and deep-links the selected card into the Explorer's address line.\nViewers can read the board. Editors can also change it.",
    "icon": { "path": "icon.svg", "digest": "c367e956895304c28f0936bd8f80db36308cfd837e8827c71ebe87940b4403a6" },
    "categories": ["productivity", "sample"],
    "publisherDisplayName": "Orleans.Lattice samples"
  },
  "trees": [{ "name": "tasks" }],
  "roles": [
    {
      "name": "viewer",
      "operations": ["Read", "RangeRead"],
      "scopes": [{ "tree": "tasks" }]
    },
    {
      "name": "editor",
      "operations": ["Read", "RangeRead", "Write", "Delete"],
      "scopes": [{ "tree": "tasks" }]
    }
  ],
  "replication": [{ "tree": "tasks", "mergeMode": "LwwRegister" }],
  "schema": [],
  "subscriptions": [],
  "mcpTools": [],
  "ui": {
    "entry": "index.html",
    "styles": ["app.css"],
    "scripts": [{ "path": "app.mjs", "module": true }],
    "assets": [
      { "path": "index.html", "mediaType": "text/html", "digest": "48e16d67c058dd9f19f64f7b27bd03fc1dfb33d39d9774be88acd988df2b8bf2" },
      { "path": "app.css", "mediaType": "text/css", "digest": "a2ad7151751a9261c28b05c7a7888bc3e6033eee044666d923a750fe22e2873b" },
      { "path": "app.mjs", "mediaType": "text/javascript", "digest": "935cc197b6148e8d984e9d7adafccbf7ced3a6f45984487bbdd04b44f867775f" },
      { "path": "icon.svg", "mediaType": "image/svg+xml", "digest": "c367e956895304c28f0936bd8f80db36308cfd837e8827c71ebe87940b4403a6" }
    ],
    "bundleDigest": "753d0f676b30113bd014d54b8dc0508bed711a2c904ac13ba88a8a2a10555c36",
    "bridge": [
      { "operation": "context.read" },
      { "operation": "data.read", "trees": ["tasks"] },
      { "operation": "data.write", "trees": ["tasks"] },
      { "operation": "data.delete", "trees": ["tasks"] },
      { "operation": "nav.sync" },
      { "operation": "ui.notify" }
    ],
    "minProtocol": 1
  }
}
````

## Apps/TaskBoard/src/TaskBoard.csproj

````xml
<Project Sdk="Microsoft.NET.Sdk">

  <!--
    The task-board sample app: a manifest and a UI bundle embedded in a small
    class library, with no build toolchain. The digests in manifest.json are
    checked (not produced) by TaskBoard.Tests, which fails with the correct
    values when a bundle file changes.
  -->
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>Orleans.Lattice.Samples.Explorer.TaskBoard</RootNamespace>
    <AssemblyName>Orleans.Lattice.Samples.Explorer.TaskBoard</AssemblyName>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <!--
    InImageAppSource reads the manifest by name and each bundle asset from
    "{manifest namespace}.ui.{path with '/' as '.'}", so these logical names
    let the three-argument AddLatticeApp overload find the bundle unaided.
  -->
  <ItemGroup>
    <EmbeddedResource Include="manifest.json" LogicalName="Orleans.Lattice.Samples.Explorer.TaskBoard.manifest.json" />
    <EmbeddedResource Include="ui/index.html" LogicalName="Orleans.Lattice.Samples.Explorer.TaskBoard.ui.index.html" />
    <EmbeddedResource Include="ui/app.css" LogicalName="Orleans.Lattice.Samples.Explorer.TaskBoard.ui.app.css" />
    <EmbeddedResource Include="ui/app.mjs" LogicalName="Orleans.Lattice.Samples.Explorer.TaskBoard.ui.app.mjs" />
    <EmbeddedResource Include="ui/icon.svg" LogicalName="Orleans.Lattice.Samples.Explorer.TaskBoard.ui.icon.svg" />
  </ItemGroup>

</Project>
````

## Apps/TaskBoard/src/TaskBoardApp.cs

````csharp
using System.Reflection;

namespace Orleans.Lattice.Samples.Explorer.TaskBoard;

/// <summary>
/// Where the task-board sample app lives in this assembly. A silo registers it with the in-image
/// app source through the three-argument <c>AddLatticeApp</c> overload:
/// <c>siloBuilder.AddLatticeApp(TaskBoardApp.Slug, TaskBoardApp.Assembly, TaskBoardApp.ManifestResourceName)</c>.
/// </summary>
public static class TaskBoardApp
{
    /// <summary>The app slug the manifest declares.</summary>
    public const string Slug = "task-board";

    /// <summary>The manifest's embedded-resource name.</summary>
    public const string ManifestResourceName = "Orleans.Lattice.Samples.Explorer.TaskBoard.manifest.json";

    /// <summary>
    /// The embedded-resource name prefix of the UI bundle assets; it is the in-image source's default
    /// for <see cref="ManifestResourceName"/>, so no explicit prefix need be passed.
    /// </summary>
    public const string AssetResourcePrefix = "Orleans.Lattice.Samples.Explorer.TaskBoard.ui.";

    /// <summary>The assembly that carries the manifest and the bundle.</summary>
    public static Assembly Assembly => typeof(TaskBoardApp).Assembly;
}
````

## Apps/TaskBoard/test/TaskBoard.Tests.csproj

````xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <RootNamespace>Orleans.Lattice.Samples.Explorer.TaskBoard.Tests</RootNamespace>
    <AssemblyName>Orleans.Lattice.Samples.Explorer.TaskBoard.Tests</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
    <PackageReference Include="NUnit" Version="4.6.1" />
    <PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="NUnit.Framework" />
  </ItemGroup>

  <!-- The tests read the bundle from disk as well as from the embedded resources. -->
  <ItemGroup>
    <AssemblyMetadata Include="TaskBoardSourceDirectory" Value="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../src/'))" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\src\TaskBoard.csproj" />
    <ProjectReference Include="..\..\..\..\..\src\lattice.apps\Orleans.Lattice.Apps.csproj" />
  </ItemGroup>

</Project>
````

## Apps/TaskBoard/test/TaskBoardBundleTests.cs

````csharp
using System.Text.RegularExpressions;

namespace Orleans.Lattice.Samples.Explorer.TaskBoard.Tests;

/// <summary>
/// The bundle is plain HTML, CSS and one self-contained ES module that reaches the Explorer only
/// through <c>globalThis.lattice</c>, needs no toolchain, and hides every write control until
/// <c>context.read</c> reports a role that may write.
/// </summary>
[TestFixture]
public sealed class TaskBoardBundleTests
{
    private static readonly string[] TextFiles =
    [
        "manifest.json", "ui/index.html", "ui/app.css", "ui/app.mjs", "ui/icon.svg", "ui/.gitattributes", ".gitattributes",
    ];

    [Test]
    public void Every_file_is_plain_ascii_with_lf_line_endings()
    {
        Assert.Multiple(() =>
        {
            foreach (var file in TextFiles)
            {
                var bytes = TaskBoardFiles.ReadBytes(file);
                Assert.That(bytes.All(b => b is 0x09 or 0x0a or (>= 0x20 and < 0x7f)), Is.True, file + " is ASCII without CR");
            }
        });
    }

    [Test]
    public void The_line_endings_are_pinned()
    {
        Assert.That(TaskBoardFiles.ReadText("ui/.gitattributes"), Does.Contain("* text eol=lf"));
        Assert.That(TaskBoardFiles.ReadText(".gitattributes"), Does.Contain("manifest.json text eol=lf"));
    }

    [TestCase(@"^\s*import\b", "a static import")]
    [TestCase(@"\bimport\s*\(", "a dynamic import")]
    [TestCase(@"^\s*export\b", "an export")]
    [TestCase(@"\bfetch\s*\(", "fetch")]
    [TestCase(@"\bXMLHttpRequest\b", "XHR")]
    [TestCase(@"\bWebSocket\b", "a WebSocket")]
    [TestCase(@"\bEventSource\b", "an EventSource")]
    [TestCase(@"\b(localStorage|sessionStorage|indexedDB)\b", "storage")]
    [TestCase(@"\bdocument\.cookie\b", "cookies")]
    [TestCase(@"\b(window\.)?parent\b|\btop\b\.|\bpostMessage\b", "the parent window")]
    [TestCase(@"\b(innerHTML|outerHTML|insertAdjacentHTML)\b|document\.write", "HTML injection")]
    [TestCase(@"\beval\s*\(|\bnew\s+Function\b", "code generation")]
    [TestCase(@"\bsetTimeout\s*\(\s*[""'`]", "string timers")]
    public void The_module_is_self_contained_and_uses_only_the_lattice_api(string pattern, string what)
    {
        var module = TaskBoardFiles.ReadText("ui/app.mjs");

        Assert.That(Regex.IsMatch(module, pattern, RegexOptions.Multiline), Is.False, "app.mjs must not use " + what);
    }

    [Test]
    public void The_module_reaches_the_host_through_the_lattice_global()
    {
        var module = TaskBoardFiles.ReadText("ui/app.mjs");

        Assert.Multiple(() =>
        {
            Assert.That(module, Does.Contain("await lattice.ready"));
            Assert.That(module, Does.Contain("lattice.on(\"context.changed\""));
            Assert.That(module, Does.Contain("lattice.on(\"nav.changed\""));
            Assert.That(module, Does.Contain("lattice.on(\"lattice.revoked\""));
            Assert.That(module, Does.Contain("lattice.assetUrl(\"icon.svg\")"));
        });
    }

    [Test]
    public void The_stylesheet_needs_no_url_import_or_hover()
    {
        var css = TaskBoardFiles.ReadText("ui/app.css");

        Assert.Multiple(() =>
        {
            Assert.That(css, Does.Not.Contain("url("), "a blob: stylesheet has no base URL");
            Assert.That(css, Does.Not.Contain("@import"));
            Assert.That(css, Does.Not.Contain(":hover"), "no hover-only affordance");
        });
    }

    [Test]
    public void Custom_controls_keep_the_kit_touch_target()
    {
        var css = TaskBoardFiles.ReadText("ui/app.css");
        var card = Regex.Match(css, @"\.tb-card\s*\{(?<body>[^}]*)\}").Groups["body"].Value;

        Assert.That(card, Does.Contain("min-height: var(--lt-app-control-height)"));
    }

    [Test]
    public void The_columns_reflow_to_one_at_a_phone_width_without_width_queries()
    {
        var css = TaskBoardFiles.ReadText("ui/app.css");

        Assert.That(css, Does.Contain("repeat(auto-fit, minmax(min(100%, 15rem), 1fr))"));
        Assert.That(css, Does.Not.Contain("@media (min-width").And.Not.Contain("@media (max-width"));
    }

    [TestCase("tb-add")]
    [TestCase("tb-detail-actions")]
    public void Write_controls_start_hidden(string id)
    {
        var html = TaskBoardFiles.ReadText("ui/index.html");

        Assert.That(Regex.IsMatch(html, "id=\"" + id + "\"[^>]*\\shidden\\b"), Is.True, id + " is hidden until context.read reports a writer role");
    }

    [Test]
    public void The_entry_has_no_form_because_the_sandbox_blocks_submission()
    {
        Assert.That(TaskBoardFiles.ReadText("ui/index.html"), Does.Not.Contain("<form"));
    }

    [Test]
    public void The_icon_is_a_static_svg()
    {
        var svg = TaskBoardFiles.ReadText("ui/icon.svg");

        Assert.Multiple(() =>
        {
            Assert.That(svg, Does.StartWith("<svg"));
            Assert.That(svg, Does.Not.Contain("<script").IgnoreCase);
            Assert.That(svg, Does.Not.Contain("href").IgnoreCase);
            Assert.That(Regex.IsMatch(svg, @"\son[a-z]+\s*=", RegexOptions.IgnoreCase), Is.False);
        });
    }
}
````

## Apps/TaskBoard/test/TaskBoardDigestTests.cs

````csharp
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Orleans.Lattice.Apps;

namespace Orleans.Lattice.Samples.Explorer.TaskBoard.Tests;

/// <summary>
/// The manifest pins the exact bytes of every bundle asset. There is no build step: these tests
/// compute the digests and, on drift, fail with the values to paste into manifest.json.
/// </summary>
[TestFixture]
public sealed class TaskBoardDigestTests
{
    private static string Sha256(byte[] bytes) => Convert.ToHexStringLower(SHA256.HashData(bytes));

    private static IReadOnlyList<AppUiAsset> ExpectedAssets()
    {
        using var document = JsonDocument.Parse(TaskBoardFiles.ReadBytes("manifest.json"));
        return document.RootElement.GetProperty("ui").GetProperty("assets").EnumerateArray()
            .Select(a =>
            {
                var path = a.GetProperty("path").GetString()!;
                return new AppUiAsset
                {
                    Path = path,
                    MediaType = a.GetProperty("mediaType").GetString()!,
                    Digest = Sha256(TaskBoardFiles.ReadAsset(path)),
                };
            })
            .ToArray();
    }

    [Test]
    public void The_manifest_digests_match_the_bundle_files()
    {
        using var document = JsonDocument.Parse(TaskBoardFiles.ReadBytes("manifest.json"));
        var root = document.RootElement;
        var ui = root.GetProperty("ui");
        var expected = ExpectedAssets();
        var expectedBundle = AppUiBundle.ComputeBundleDigest(expected.ToArray());
        var expectedIcon = expected.Single(a => a.Path == "icon.svg").Digest;

        var drift = new StringBuilder();
        foreach (var (declared, asset) in ui.GetProperty("assets").EnumerateArray().Zip(expected))
        {
            if (declared.GetProperty("digest").GetString() != asset.Digest)
                drift.AppendLine($"  ui.assets[{asset.Path}].digest = \"{asset.Digest}\"");
        }
        if (ui.GetProperty("bundleDigest").GetString() != expectedBundle)
            drift.AppendLine($"  ui.bundleDigest = \"{expectedBundle}\"");
        if (root.GetProperty("presentation").GetProperty("icon").GetProperty("digest").GetString() != expectedIcon)
            drift.AppendLine($"  presentation.icon.digest = \"{expectedIcon}\"");

        Assert.That(drift.ToString(), Is.Empty, "manifest.json has drifted from the bundle files. Set:" + Environment.NewLine + drift);
    }

    [Test]
    public void The_manifest_lists_exactly_the_bundle_files()
    {
        var onDisk = Directory.EnumerateFiles(Path.Combine(TaskBoardFiles.SourceDirectory, "ui"))
            .Select(Path.GetFileName)
            .Where(name => name != ".gitattributes");

        Assert.That(TaskBoardFiles.Manifest().Ui!.Assets.Select(a => a.Path), Is.EquivalentTo(onDisk));
        Assert.That(TaskBoardFiles.Manifest().Ui!.Assets.Select(a => a.Path), Is.EqualTo(TaskBoardFiles.AssetPaths));
    }

    [Test]
    public void The_bundle_digest_is_the_f1_digest_of_the_assets()
    {
        var ui = TaskBoardFiles.Manifest().Ui!;

        Assert.That(AppUiBundle.ComputeBundleDigest(ui.Assets), Is.EqualTo(ui.BundleDigest));
    }

    [Test]
    public void The_icon_digest_is_its_asset_digest()
    {
        var manifest = TaskBoardFiles.Manifest();

        Assert.That(manifest.Presentation!.Icon!.Digest, Is.EqualTo(manifest.Ui!.Assets.Single(a => a.Path == "icon.svg").Digest));
    }

    [Test]
    public void Every_embedded_resource_is_the_file_on_disk()
    {
        Assert.Multiple(() =>
        {
            Assert.That(TaskBoardFiles.ReadEmbedded(TaskBoardApp.ManifestResourceName), Is.EqualTo(TaskBoardFiles.ReadBytes("manifest.json")), "manifest.json");
            foreach (var path in TaskBoardFiles.AssetPaths)
            {
                Assert.That(TaskBoardFiles.ReadEmbedded(TaskBoardApp.AssetResourcePrefix + path), Is.EqualTo(TaskBoardFiles.ReadAsset(path)), path);
            }
        });
    }
}
````

## Apps/TaskBoard/test/TaskBoardFiles.cs

````csharp
using System.Reflection;
using Orleans.Lattice.Apps;

namespace Orleans.Lattice.Samples.Explorer.TaskBoard.Tests;

/// <summary>Reads the task board's manifest and bundle, from disk and from the embedded resources.</summary>
internal static class TaskBoardFiles
{
    /// <summary>The bundle assets the manifest lists, in manifest order.</summary>
    public static readonly string[] AssetPaths = ["index.html", "app.css", "app.mjs", "icon.svg"];

    /// <summary>The app library's source directory, stamped into this assembly at build time.</summary>
    public static string SourceDirectory { get; } = typeof(TaskBoardFiles).Assembly
        .GetCustomAttributes<AssemblyMetadataAttribute>()
        .Single(a => a.Key == "TaskBoardSourceDirectory").Value!;

    /// <summary>Reads the bytes of a file under the app's source directory.</summary>
    public static byte[] ReadBytes(string relative) =>
        File.ReadAllBytes(Path.Combine(SourceDirectory, relative.Replace('/', Path.DirectorySeparatorChar)));

    /// <summary>Reads a file under the app's source directory as text.</summary>
    public static string ReadText(string relative) =>
        File.ReadAllText(Path.Combine(SourceDirectory, relative.Replace('/', Path.DirectorySeparatorChar)));

    /// <summary>Reads a bundle asset's bytes from disk.</summary>
    public static byte[] ReadAsset(string path) => ReadBytes("ui/" + path);

    /// <summary>Reads an embedded resource of the app assembly.</summary>
    public static byte[] ReadEmbedded(string resourceName)
    {
        using var stream = TaskBoardApp.Assembly.GetManifestResourceStream(resourceName)
            ?? throw new AssertionException($"The app assembly has no embedded resource '{resourceName}'.");
        using var buffer = new MemoryStream();
        stream.CopyTo(buffer);
        return buffer.ToArray();
    }

    /// <summary>Parses the embedded manifest, failing with every diagnostic when it does not validate.</summary>
    public static AppManifest Manifest()
    {
        using var stream = new MemoryStream(ReadEmbedded(TaskBoardApp.ManifestResourceName));
        var result = AppManifestParser.Parse(stream);
        Assert.That(result.Errors, Is.Empty, string.Join("; ", result.Errors.Select(e => e.Path + ": " + e.Message)));
        return result.Manifest!;
    }
}
````

## Apps/TaskBoard/test/TaskBoardInImageSourceTests.cs

````csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Orleans.Lattice.Apps;
using Orleans.Lattice.Apps.Sources;

namespace Orleans.Lattice.Samples.Explorer.TaskBoard.Tests;

/// <summary>
/// The task board registers through the same <c>AddLatticeApp</c> line the Explorer sample uses,
/// and the in-image source then enumerates, resolves and serves it.
/// </summary>
[TestFixture]
public sealed class TaskBoardInImageSourceTests
{
    private static InImageAppSource Source()
    {
        var services = new ServiceCollection();
        services.AddLatticeApp(TaskBoardApp.Slug, TaskBoardApp.Assembly, TaskBoardApp.ManifestResourceName);
        using var provider = services.BuildServiceProvider();
        return new InImageAppSource(provider.GetRequiredService<IOptions<InImageAppSourceOptions>>());
    }

    [Test]
    public void The_default_asset_prefix_is_the_one_the_app_embeds_under()
    {
        var registration = new InImageAppRegistration(AppSlug.Parse(TaskBoardApp.Slug), TaskBoardApp.Assembly, TaskBoardApp.ManifestResourceName);

        Assert.That(registration.AssetResourcePrefix, Is.EqualTo(TaskBoardApp.AssetResourcePrefix));
    }

    [Test]
    public async Task The_app_enumerates_from_the_in_image_source()
    {
        var source = Source();

        var page = await source.ListAsync(AppSourceQuery.Default);

        Assert.Multiple(() =>
        {
            Assert.That(source.Descriptor.Key, Is.EqualTo("in-image"));
            Assert.That(page.Entries, Has.Count.EqualTo(1));
            var entry = page.Entries[0];
            Assert.That(entry.Slug.Value, Is.EqualTo(TaskBoardApp.Slug));
            Assert.That(entry.IsAvailable, Is.True, string.Join("; ", entry.Errors.Select(e => e.Path + ": " + e.Message)));
            Assert.That(entry.Versions, Is.EqualTo(new[] { AppVersion.Parse("1.0.0") }));
            Assert.That(entry.Provenance!.Source, Is.EqualTo(InImageAppSource.SourceKey));
            Assert.That(entry.Manifest!.Presentation!.DisplayName, Is.EqualTo("Task board"));
        });
    }

    [Test]
    public async Task The_app_resolves_from_the_composed_source_set()
    {
        var set = new AppSourceSet([Source()]);

        var result = await set.ResolveAsync(AppSlug.Parse(TaskBoardApp.Slug), sourceKey: InImageAppSource.SourceKey);

        Assert.That(result.IsResolved, Is.True, string.Join("; ", result.Errors.Select(e => e.Path + ": " + e.Message)));
        Assert.That(result.Manifest!.Ui, Is.Not.Null);
    }

    [Test]
    public async Task Every_bundle_asset_opens_verified_from_the_in_image_source()
    {
        var source = Source();
        var manifest = TaskBoardFiles.Manifest();

        foreach (var asset in manifest.Ui!.Assets)
        {
            var opened = await source.OpenAssetAsync(manifest.Identity.Slug, manifest.Identity.Version, asset.Path, asset.Digest);

            Assert.That(opened.IsOpened, Is.True, asset.Path + ": " + opened.Status);
            Assert.That(opened.MediaType, Is.EqualTo(asset.MediaType), asset.Path);
            Assert.That(opened.Content.ToArray(), Is.EqualTo(TaskBoardFiles.ReadAsset(asset.Path)), asset.Path);
        }
    }
}
````

## Apps/TaskBoard/test/TaskBoardManifestTests.cs

````csharp
using System.Text.RegularExpressions;
using Orleans.Lattice.Apps;
using Orleans.Lattice.Auth;

namespace Orleans.Lattice.Samples.Explorer.TaskBoard.Tests;

/// <summary>The task board's manifest: one tree, two roles, a presentation and a UI that asks only for what it uses.</summary>
[TestFixture]
public sealed class TaskBoardManifestTests
{
    [Test]
    public void The_manifest_validates()
    {
        using var stream = new MemoryStream(TaskBoardFiles.ReadEmbedded(TaskBoardApp.ManifestResourceName));
        var result = AppManifestParser.Parse(stream);

        Assert.That(result.IsValid, Is.True, string.Join("; ", result.Errors.Select(e => e.Path + ": " + e.Message)));
    }

    [Test]
    public void The_identity_is_the_task_board_slug()
    {
        var identity = TaskBoardFiles.Manifest().Identity;

        Assert.That(identity.Slug.Value, Is.EqualTo(TaskBoardApp.Slug));
        Assert.That(identity.Provenance.Source, Is.EqualTo(InImageAppSource.SourceKey));
    }

    [Test]
    public void The_app_declares_one_tree_named_tasks()
    {
        Assert.That(TaskBoardFiles.Manifest().Trees.Select(t => t.Name), Is.EqualTo(new[] { "tasks" }));
    }

    [Test]
    public void The_viewer_reads_and_the_editor_reads_and_writes_only_its_own_tree()
    {
        var roles = TaskBoardFiles.Manifest().Roles.ToDictionary(r => r.Name);

        Assert.Multiple(() =>
        {
            Assert.That(roles.Keys, Is.EquivalentTo(new[] { "viewer", "editor" }));
            Assert.That(roles["viewer"].Operations, Is.EqualTo(LatticeOperation.Read | LatticeOperation.RangeRead));
            Assert.That(roles["editor"].Operations,
                Is.EqualTo(LatticeOperation.Read | LatticeOperation.RangeRead | LatticeOperation.Write | LatticeOperation.Delete));
            foreach (var role in roles.Values)
            {
                Assert.That(role.Scopes, Has.Length.EqualTo(1), role.Name);
                Assert.That(role.Scopes[0].Tree, Is.EqualTo("tasks"), role.Name);
                Assert.That(role.Scopes[0].App, Is.Null, role.Name + " stays in the app's own namespace");
                Assert.That(role.Scopes[0].Kind, Is.EqualTo(LatticeScopeKind.Tree), role.Name);
            }
        });
    }

    [Test]
    public void The_presentation_has_a_name_summary_description_and_svg_icon()
    {
        var presentation = TaskBoardFiles.Manifest().Presentation!;

        Assert.Multiple(() =>
        {
            Assert.That(presentation.DisplayName, Is.EqualTo("Task board"));
            Assert.That(presentation.Summary, Is.Not.Null.And.Not.Empty);
            Assert.That(presentation.Description, Is.Not.Null.And.Not.Empty);
            Assert.That(presentation.Icon!.Path, Is.EqualTo("icon.svg"));
        });
    }

    [Test]
    public void The_ui_is_a_fragment_one_stylesheet_and_one_module()
    {
        var ui = TaskBoardFiles.Manifest().Ui!;

        Assert.Multiple(() =>
        {
            Assert.That(ui.Entry, Is.EqualTo("index.html"));
            Assert.That(ui.Styles, Is.EqualTo(new[] { "app.css" }));
            Assert.That(ui.Scripts, Has.Length.EqualTo(1));
            Assert.That(ui.Scripts![0].Path, Is.EqualTo("app.mjs"));
            Assert.That(ui.Scripts[0].Module, Is.True);
            Assert.That(ui.MinProtocol, Is.EqualTo(AppUiProtocol.Current));
        });
    }

    [Test]
    public void The_ui_requests_exactly_the_pilot_bridge_operations()
    {
        var bridge = TaskBoardFiles.Manifest().Ui!.Bridge!;

        Assert.That(bridge.Select(b => b.Operation), Is.EqualTo(new[]
        {
            AppUiBridgeOperations.ContextRead,
            AppUiBridgeOperations.DataRead,
            AppUiBridgeOperations.DataWrite,
            AppUiBridgeOperations.DataDelete,
            AppUiBridgeOperations.NavSync,
            AppUiBridgeOperations.UiNotify,
        }));
        Assert.That(bridge.Where(b => AppUiBridgeOperations.IsDataOperation(b.Operation)).Select(b => b.Trees),
            Is.All.EqualTo(new[] { "tasks" }));
    }

    [Test]
    public void The_writer_roles_in_the_module_are_the_manifest_roles_that_can_write()
    {
        var module = TaskBoardFiles.ReadText("ui/app.mjs");
        var declared = Regex.Match(module, @"const WRITER_ROLES = \[(?<list>[^\]]*)\];");
        Assert.That(declared.Success, Is.True, "app.mjs declares WRITER_ROLES");
        var inModule = Regex.Matches(declared.Groups["list"].Value, "\"(?<name>[a-z][a-z0-9_-]*)\"").Select(m => m.Groups["name"].Value);

        var writers = TaskBoardFiles.Manifest().Roles
            .Where(r => r.Operations.HasFlag(LatticeOperation.Write) && r.Operations.HasFlag(LatticeOperation.Delete))
            .Select(r => r.Name);

        Assert.That(inModule, Is.EquivalentTo(writers));
        Assert.That(writers, Is.EquivalentTo(new[] { "editor" }));
    }

    [Test]
    public void The_board_decides_write_access_from_context_read_roles_not_a_probe()
    {
        var module = TaskBoardFiles.ReadText("ui/app.mjs");

        Assert.Multiple(() =>
        {
            Assert.That(module, Does.Contain("context.roles"));
            Assert.That(module, Does.Contain("Array.isArray(context.roles)"), "an absent roles member infers no role");
            Assert.That(module, Does.Not.Contain("probe").IgnoreCase, "no write probe");
            Assert.That(Regex.Matches(module, "request\\(\"data\\.delete\"").Count, Is.EqualTo(1), "the only delete is the user's own delete");
            Assert.That(Regex.IsMatch(module, @"canEdit:\s*false"), Is.True, "the board starts read-only");
            Assert.That(module, Does.Contain("code === \"denied\""), "a denied write drops to read-only");
        });
    }

    [Test]
    public void The_module_uses_every_operation_it_requests_and_no_other()
    {
        var bridge = TaskBoardFiles.Manifest().Ui!.Bridge!.Select(b => b.Operation).ToHashSet();
        var module = TaskBoardFiles.ReadText("ui/app.mjs");

        Assert.Multiple(() =>
        {
            foreach (var operation in AppUiBridgeOperations.All)
            {
                var used = module.Contains("request(\"" + operation + "\"", StringComparison.Ordinal);
                Assert.That(used, Is.EqualTo(bridge.Contains(operation)), operation);
            }
        });
    }

    [Test]
    public void The_entry_is_a_valid_fragment()
    {
        Assert.That(AppManifestValidator.ValidateUiEntryFragment(TaskBoardFiles.ReadAsset("index.html")), Is.Empty);
    }
}
````

## DemoBasicAuthenticator.cs

````csharp
using System.Text;
using Orleans.Lattice;
using Orleans.Lattice.Membership;

namespace Orleans.Lattice.Samples.Explorer;

/// <summary>
/// A minimal demo <see cref="ILatticeCredentialAuthenticator"/> that trusts the
/// username in the console's auto-applied Basic sign-in as the caller subject id.
/// The Explorer web console signs in with
/// <c>authorization: Basic base64(username:password)</c>; the auth / schema gRPC
/// bindings (configured with a <c>Basic</c> credential scheme) strip the scheme
/// and hand this authenticator the base64 token, which it decodes to recover the
/// username. That username is the caller subject, and the sample registers it as
/// a bootstrap administrator so the Access and Schema admin areas light up.
///
/// A real deployment resolves the subject from a validated JWT or Entra token
/// (see the JWT / Entra authenticators shipped with the Membership package); this
/// sample uses a trivial trusted-token authenticator so the whole flow runs on
/// one silo with no identity provider and the password is never checked.
/// </summary>
internal sealed class DemoBasicAuthenticator : ILatticeCredentialAuthenticator
{
    /// <summary>The credential scheme this authenticator claims.</summary>
    public const string Scheme = "Basic";

    /// <summary>The issuer stamped on the resolved principal.</summary>
    public const string Issuer = "https://issuer.explorer.sample/";

    /// <inheritdoc />
    public bool CanHandle(in LatticeCredential credential) =>
        string.Equals(credential.Scheme, Scheme, StringComparison.OrdinalIgnoreCase);

    /// <inheritdoc />
    public ValueTask<LatticePrincipal?> AuthenticateAsync(
        LatticeCredential credential,
        CancellationToken cancellationToken = default)
    {
        // The token is base64(username:password); the caller subject is the
        // username. A malformed token resolves to no principal (anonymous).
        string username;
        try
        {
            var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(credential.Token));
            var separator = decoded.IndexOf(':');
            username = separator >= 0 ? decoded[..separator] : decoded;
        }
        catch (FormatException)
        {
            return new ValueTask<LatticePrincipal?>((LatticePrincipal?)null);
        }

        return string.IsNullOrEmpty(username)
            ? new ValueTask<LatticePrincipal?>((LatticePrincipal?)null)
            : new ValueTask<LatticePrincipal?>(new LatticePrincipal(username, Issuer));
    }
}
````

## Explorer.csproj

````xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>Orleans.Lattice.Samples.Explorer</RootNamespace>
    <AssemblyName>Orleans.Lattice.Samples.Explorer</AssemblyName>
    <IsPackable>false</IsPackable>
    <!--
      The Explorer's Razor components (including the App root and interactive
      server render mode) live in the referenced RCLs, not this host project, so
      the SDK does not auto-detect that this app hosts Blazor and omits the
      framework's _framework/blazor.web.js script - leaving the console rendered
      but non-interactive. Opting in explicitly pulls the Blazor Web script into
      this app's static web assets so the interactive circuit starts.
    -->
    <RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
  </PropertyGroup>

  <!--
    Apps/ holds sample apps as their own projects and test/ holds this sample's
    smoke tests; keep this host's default globs out of both.
  -->
  <ItemGroup>
    <Compile Remove="Apps/**;test/**" />
    <Content Remove="Apps/**;test/**" />
    <None Remove="Apps/**;test/**" />
    <EmbeddedResource Remove="Apps/**;test/**" />
  </ItemGroup>

  <ItemGroup>
    <InternalsVisibleTo Include="Orleans.Lattice.Samples.Explorer.Tests" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Orleans.Server" Version="10.2.2" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\lattice\Orleans.Lattice.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.state\Orleans.Lattice.Api.State.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.state.grpc\Orleans.Lattice.Api.State.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.membership\Orleans.Lattice.Membership.csproj" />
    <ProjectReference Include="..\..\src\lattice.membership.entra\Orleans.Lattice.Membership.Entra.csproj" />
    <ProjectReference Include="..\..\src\lattice.membership.entra.graph\Orleans.Lattice.Membership.Entra.Graph.csproj" />
    <ProjectReference Include="..\..\src\lattice.auth\Orleans.Lattice.Auth.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.auth\Orleans.Lattice.Api.Auth.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.auth.grpc\Orleans.Lattice.Api.Auth.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.schema\Orleans.Lattice.Schema.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.schema\Orleans.Lattice.Api.Schema.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.schema.grpc\Orleans.Lattice.Api.Schema.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.explorer\WebHosting\Orleans.Lattice.Explorer.Web.csproj" />
    <ProjectReference Include="..\..\src\lattice.apps\Orleans.Lattice.Apps.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.apps\Orleans.Lattice.Api.Apps.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.apps.grpc\Orleans.Lattice.Api.Apps.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.treeadmin\Orleans.Lattice.Api.TreeAdmin.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.treeadmin.grpc\Orleans.Lattice.Api.TreeAdmin.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.backup\Orleans.Lattice.Backup.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.backup\Orleans.Lattice.Api.Backup.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.backup.grpc\Orleans.Lattice.Api.Backup.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.replication\Orleans.Lattice.Replication.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.replication\Orleans.Lattice.Api.Replication.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.replication.grpc\Orleans.Lattice.Api.Replication.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.replication.grpc\Orleans.Lattice.Replication.Grpc.csproj" />
    <ProjectReference Include="..\..\src\lattice.tenancy\Orleans.Lattice.Tenancy.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.tenantadmin\Orleans.Lattice.Api.TenantAdmin.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.tenantadmin.grpc\Orleans.Lattice.Api.TenantAdmin.Grpc.csproj" />
    <!-- The task-board pilot app (P1): a manifest and UI bundle embedded in its own class library. -->
    <ProjectReference Include="Apps\TaskBoard\src\TaskBoard.csproj" />
  </ItemGroup>

</Project>
````

## ExplorerSample.cs

````csharp
namespace Orleans.Lattice.Samples.Explorer;

/// <summary>
/// The whole sample: the east region, which serves the Explorer console, and -
/// unless it runs <c>--minimal</c> - the west region it replicates with, the
/// backup sink they share, the switch that pauses the link between them and
/// the background writer that keeps the link busy.
/// </summary>
internal sealed class ExplorerSample : IAsyncDisposable
{
    private readonly List<string> _seedLog = [];
    private readonly Lock _seedLogLock = new();

    private ExplorerSample(
        ExplorerSampleOptions options,
        SampleRegion east,
        SampleRegion? west,
        SampleSharedBackupSink? sink,
        PeerLink peerLink,
        SampleConsolePlan console)
    {
        Options = options;
        East = east;
        West = west;
        Sink = sink;
        PeerLink = peerLink;
        Console = console;
    }

    /// <summary>The options the sample was built with.</summary>
    public ExplorerSampleOptions Options { get; }

    /// <summary>The primary region, which serves the Explorer console.</summary>
    public SampleRegion East { get; }

    /// <summary>The peer region, or <see langword="null"/> when the sample runs <c>--minimal</c>.</summary>
    public SampleRegion? West { get; }

    /// <summary>The backup sink the two regions share, or <see langword="null"/> when the sample runs <c>--minimal</c>.</summary>
    public SampleSharedBackupSink? Sink { get; }

    /// <summary>The switch that pauses replication between the regions.</summary>
    public PeerLink PeerLink { get; }

    /// <summary>Where the console is served and which region it connects to.</summary>
    public SampleConsolePlan Console { get; }

    /// <summary>The background writer, once started; <see langword="null"/> when the sample runs <c>--minimal</c>.</summary>
    public ReplicationWriter? Writer { get; private set; }

    /// <summary>The region the console connects to.</summary>
    public SampleRegion ConsoleRegion => West is { } west && Options.ExplorerRegion == west.Id ? west : East;

    /// <summary>Every region, primary first.</summary>
    public IReadOnlyList<SampleRegion> Regions => West is null ? [East] : [East, West];

    /// <summary>One line per item seeded, once <see cref="StartAsync"/> has completed.</summary>
    public IReadOnlyList<string> SeedLog
    {
        get
        {
            lock (_seedLogLock)
            {
                return [.. _seedLog];
            }
        }
    }

    /// <summary>Builds the sample's regions; nothing starts until <see cref="StartAsync"/>.</summary>
    /// <param name="options">How the sample runs.</param>
    /// <returns>The sample.</returns>
    public static ExplorerSample Create(ExplorerSampleOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        var ports = options.Ports;
        var peerLink = new PeerLink();
        var eastEndpoint = new Uri($"http://localhost:{ports.EastGrpc}");
        if (options.Minimal)
        {
            var console = new SampleConsolePlan(ports.EastWeb, eastEndpoint, options.ExplorerConfigPath, options.SignInAs);
            var single = SampleRegion.Build(
                new SampleRegionPlan(SampleIdentities.EastRegion, ports.EastGrpc, ports.EastSilo, ports.EastGateway, Peer: null, console),
                options,
                sink: null,
                peerLink);
            return new ExplorerSample(options, single, west: null, sink: null, peerLink, console);
        }

        var westEndpoint = new Uri($"http://localhost:{ports.WestGrpc}");
        var estateConsole = new SampleConsolePlan(
            ports.EastWeb,
            options.ExplorerRegion == SampleIdentities.WestRegion ? westEndpoint : eastEndpoint,
            options.ExplorerConfigPath,
            options.SignInAs);
        var sink = new SampleSharedBackupSink();
        var east = SampleRegion.Build(
            new SampleRegionPlan(
                SampleIdentities.EastRegion,
                ports.EastGrpc,
                ports.EastSilo,
                ports.EastGateway,
                new SampleRegionPeer(SampleIdentities.WestRegion, ports.WestGrpc),
                estateConsole),
            options,
            sink,
            peerLink);
        var west = SampleRegion.Build(
            new SampleRegionPlan(
                SampleIdentities.WestRegion,
                ports.WestGrpc,
                ports.WestSilo,
                ports.WestGateway,
                new SampleRegionPeer(SampleIdentities.EastRegion, ports.EastGrpc),
                Console: null),
            options,
            sink,
            peerLink);
        return new ExplorerSample(options, east, west, sink, peerLink, estateConsole);
    }

    /// <summary>
    /// Starts every region, seeds them and, on the estate, starts the background
    /// writer. The console's persisted configuration is cleared first, so it
    /// connects to the region this run was asked for.
    /// </summary>
    /// <param name="cancellationToken">Cancels the start.</param>
    public async Task StartAsync(CancellationToken cancellationToken = default)
    {
        if (File.Exists(Console.ConfigPath))
        {
            File.Delete(Console.ConfigPath);
        }

        await Task.WhenAll(Regions.Select(region => region.StartAsync(cancellationToken))).ConfigureAwait(false);

        // Every region is seeded, and the demo tree enrolled and known to the
        // peer, before the primary writes the data replication then carries.
        var staticDirectory = Options.Entra is null;
        await Task.WhenAll(Regions.Select(region => SampleSeeder.SeedRegionAsync(region, staticDirectory, Log, cancellationToken)))
            .ConfigureAwait(false);
        if (West is { } peer)
        {
            await SampleSeeder.EnrolAsync(East, peer, Log, cancellationToken).ConfigureAwait(false);
        }

        await SampleSeeder.SeedPrimaryAsync(East, West, Log, cancellationToken).ConfigureAwait(false);

        if (West is { } west)
        {
            var grainsEast = East.Services.GetRequiredService<IGrainFactory>();
            var grainsWest = west.Services.GetRequiredService<IGrainFactory>();
            Writer = new ReplicationWriter(
                [
                    new ReplicationWriterTarget(East.Id, grainsEast.GetGrain<ILattice>(SampleIdentities.FactoryFloorTree), "machine-", SampleIdentities.MachineCount),
                    new ReplicationWriterTarget(west.Id, grainsWest.GetGrain<ILattice>(SampleIdentities.FactoryFloorTree), "west-sensor-", 4),
                ],
                Options.WriterInterval);
            Writer.Start();

            if (Options.StartPeerPaused)
            {
                // Paused only once the seeded data has reached the peer, so every
                // link has made contact: from there the links age into Lagging
                // and then Stalled, rather than reading as never contacted.
                var arrived = await WaitForSeedOnPeerAsync(west, SeedArrivalBudget, cancellationToken).ConfigureAwait(false);
                PeerLink.Pause();
                Log(arrived
                    ? $"[{East.Id}] Peer link paused once the seeded data reached '{west.Id}'."
                    : $"[{East.Id}] Peer link paused; the seeded data had not reached '{west.Id}' within {SeedArrivalBudget.TotalSeconds:0}s.");
            }
        }
    }

    /// <summary>How long <c>--peer-paused</c> waits for the seeded data to reach the peer before pausing regardless.</summary>
    public static TimeSpan SeedArrivalBudget { get; } = TimeSpan.FromSeconds(20);

    /// <summary>
    /// Waits until the last seeded machine of the demo tree and the last seeded
    /// card of acme's task board have both replicated to <paramref name="peer"/>,
    /// or <paramref name="budget"/> elapses.
    /// </summary>
    /// <param name="peer">The peer region.</param>
    /// <param name="budget">The longest wait.</param>
    /// <param name="cancellationToken">Cancels the wait.</param>
    /// <returns>Whether the data arrived.</returns>
    public static async Task<bool> WaitForSeedOnPeerAsync(SampleRegion peer, TimeSpan budget, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(peer);

        var grains = peer.Services.GetRequiredService<IGrainFactory>();
        var expected = new (ILattice Tree, string Key)[]
        {
            (grains.GetGrain<ILattice>(SampleIdentities.FactoryFloorTree), SampleSeeder.MachineKey(SampleIdentities.MachineCount - 1)),
            (grains.GetGrain<ILattice>(SampleSeeder.TaskBoardTree(SampleIdentities.AcmeTenant)), SampleSeeder.TaskKey(SampleSeeder.AcmeTasks[^1].Id)),
        };
        using var budgetSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        budgetSource.CancelAfter(budget);
        using var poll = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
        try
        {
            do
            {
                using (LatticeSystemOrigin.Enter())
                {
                    var arrived = true;
                    foreach (var (tree, key) in expected)
                    {
                        arrived &= await tree.GetAsync(key, budgetSource.Token).ConfigureAwait(false) is not null;
                    }

                    if (arrived)
                    {
                        return true;
                    }
                }
            }
            while (await poll.WaitForNextTickAsync(budgetSource.Token).ConfigureAwait(false));
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            // The budget elapsed.
        }

        return false;
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        if (Writer is not null)
        {
            await Writer.DisposeAsync().ConfigureAwait(false);
        }

        foreach (var region in Regions)
        {
            await region.DisposeAsync().ConfigureAwait(false);
        }
    }

    private void Log(string line)
    {
        lock (_seedLogLock)
        {
            _seedLog.Add(line);
        }
    }
}
````

## ExplorerSampleOptions.cs

````csharp
using System.Diagnostics.CodeAnalysis;
using Orleans.Lattice.Membership;

namespace Orleans.Lattice.Samples.Explorer;

/// <summary>
/// How the sample runs: the full two-region estate with tenancy (the default),
/// or <c>--minimal</c> for the single-cluster experience; which region the
/// Explorer connects to; and the identity-directory and group-merge settings
/// read from the environment.
/// </summary>
internal sealed class ExplorerSampleOptions
{
    /// <summary>The switch that keeps the single-cluster experience.</summary>
    public const string MinimalSwitch = "--minimal";

    /// <summary>The switch that picks the region the Explorer connects to; it takes <c>east</c> or <c>west</c>.</summary>
    public const string ExplorerRegionSwitch = "--explorer-region";

    /// <summary>The switch that pauses the link to the peer region as soon as the seeded data has reached it.</summary>
    public const string PeerPausedSwitch = "--peer-paused";

    /// <summary>The switch that picks the identity the console signs in as automatically, or <c>none</c>.</summary>
    public const string SignInAsSwitch = "--sign-in-as";

    /// <summary>The <see cref="SignInAsSwitch"/> value that turns the automatic sign-in off.</summary>
    public const string NoSignIn = "none";

    /// <summary>The switch that shifts every port by a number, for when the default ports are taken.</summary>
    public const string PortOffsetSwitch = "--port-offset";

    /// <summary>The largest <see cref="PortOffsetSwitch"/> accepted, which keeps every port below 65536.</summary>
    public const int MaxPortOffset = 30000;

    /// <summary>The Entra tenant variable; set it with the other two to use the Entra identity directory.</summary>
    public const string EntraTenantIdVariable = "LATTICE_ENTRA_TENANT_ID";

    /// <summary>The Entra client-id variable.</summary>
    public const string EntraClientIdVariable = "LATTICE_ENTRA_CLIENT_ID";

    /// <summary>The Entra client-secret variable.</summary>
    public const string EntraClientSecretVariable = "LATTICE_ENTRA_CLIENT_SECRET";

    /// <summary>The group-merge mode variable: <c>Union</c> (default), <c>TokenOnly</c> or <c>DirectoryOnly</c>.</summary>
    public const string MergeModeVariable = "LATTICE_MEMBERSHIP_MERGE_MODE";

    /// <summary>The usage line printed with a rejected argument.</summary>
    public const string Usage = "Usage: dotnet run [-- [--minimal] [--explorer-region east|west] [--sign-in-as <user>|none] [--peer-paused] [--port-offset <n>]]";

    /// <summary>Whether to run one single-region cluster without tenancy, as the sample did before the estate.</summary>
    public bool Minimal { get; init; }

    /// <summary>The region the Explorer console connects to.</summary>
    public string ExplorerRegion { get; init; } = SampleIdentities.EastRegion;

    /// <summary>
    /// The identity the console signs in as automatically, or <see langword="null"/>
    /// to leave it signed out so the sign-in dialog picks the identity. Signing out
    /// does not stick while an automatic sign-in is set: the console signs straight
    /// back in.
    /// </summary>
    public string? SignInAs { get; init; } = SampleIdentities.Administrator;

    /// <summary>Whether to pause the link to the peer region once the seeded data has reached it.</summary>
    public bool StartPeerPaused { get; init; }

    /// <summary>The Entra registration backing the identity directory, or <see langword="null"/> for the static roster.</summary>
    public SampleEntraDirectory? Entra { get; init; }

    /// <summary>Whether locally-defined membership contributes to authorization.</summary>
    public SubjectGroupMergeMode GroupMergeMode { get; init; } = SubjectGroupMergeMode.Union;

    /// <summary>The ports the sample binds.</summary>
    public SamplePorts Ports { get; init; } = SamplePorts.Default;

    /// <summary>
    /// The Explorer console's persisted configuration file. The sample deletes it
    /// on start, so every run connects to the region it was asked for.
    /// </summary>
    public string ExplorerConfigPath { get; init; } = Path.Combine(AppContext.BaseDirectory, "explorer-sample-config.json");

    /// <summary>How often the background writer updates one replicated key.</summary>
    public TimeSpan WriterInterval { get; init; } = TimeSpan.FromSeconds(1);

    /// <summary>
    /// Reads the command line and the environment, rejecting anything it does not
    /// recognise so a typo never silently runs a different sample.
    /// </summary>
    /// <param name="args">The command-line arguments.</param>
    /// <param name="environment">Reads an environment variable, returning <see langword="null"/> when it is unset.</param>
    /// <param name="options">The options, when the input is valid.</param>
    /// <param name="error">Why the input was rejected, when it is not.</param>
    /// <returns>Whether the input was valid.</returns>
    public static bool TryParse(
        IReadOnlyList<string> args,
        Func<string, string?> environment,
        [NotNullWhen(true)] out ExplorerSampleOptions? options,
        [NotNullWhen(false)] out string? error)
    {
        ArgumentNullException.ThrowIfNull(args);
        ArgumentNullException.ThrowIfNull(environment);

        options = null;
        var minimal = false;
        var peerPaused = false;
        var portOffset = 0;
        string? region = null;
        var signInAs = SampleIdentities.Administrator;

        for (var i = 0; i < args.Count; i++)
        {
            var arg = args[i];
            if (string.Equals(arg, MinimalSwitch, StringComparison.OrdinalIgnoreCase))
            {
                minimal = true;
            }
            else if (string.Equals(arg, PeerPausedSwitch, StringComparison.OrdinalIgnoreCase))
            {
                peerPaused = true;
            }
            else if (string.Equals(arg, ExplorerRegionSwitch, StringComparison.OrdinalIgnoreCase))
            {
                if (i + 1 >= args.Count)
                {
                    error = $"{ExplorerRegionSwitch} needs a region: {SampleIdentities.EastRegion} or {SampleIdentities.WestRegion}.";
                    return false;
                }

                region = args[++i].Trim().ToLowerInvariant();
                if (region is not (SampleIdentities.EastRegion or SampleIdentities.WestRegion))
                {
                    error = $"{ExplorerRegionSwitch} '{args[i]}' is not a region: use {SampleIdentities.EastRegion} or {SampleIdentities.WestRegion}.";
                    return false;
                }
            }
            else if (string.Equals(arg, SignInAsSwitch, StringComparison.OrdinalIgnoreCase))
            {
                var value = i + 1 < args.Count ? args[++i].Trim() : string.Empty;
                if (value.Length == 0 || value.StartsWith('-') || value.Contains(':', StringComparison.Ordinal) || value.Any(char.IsWhiteSpace))
                {
                    error = $"{SignInAsSwitch} needs a user name, such as {SampleIdentities.AcmeAdmin}, or {NoSignIn}.";
                    return false;
                }

                signInAs = value;
            }
            else if (string.Equals(arg, PortOffsetSwitch, StringComparison.OrdinalIgnoreCase))
            {
                if (i + 1 >= args.Count
                    || !int.TryParse(args[++i], System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out portOffset)
                    || portOffset > MaxPortOffset)
                {
                    error = $"{PortOffsetSwitch} needs a whole number from 0 to {MaxPortOffset}.";
                    return false;
                }
            }
            else
            {
                error = $"'{arg}' is not recognised. {Usage}";
                return false;
            }
        }

        if (minimal && (peerPaused || region == SampleIdentities.WestRegion))
        {
            error = $"{MinimalSwitch} runs one region with no peer, so it cannot be combined with {PeerPausedSwitch} or {ExplorerRegionSwitch} {SampleIdentities.WestRegion}.";
            return false;
        }

        if (!TryReadEntra(environment, out var entra, out var entraError))
        {
            error = entraError;
            return false;
        }

        var mergeModeVariable = environment(MergeModeVariable);
        var mergeMode = SubjectGroupMergeMode.Union;
        if (!string.IsNullOrWhiteSpace(mergeModeVariable)
            && (!Enum.TryParse(mergeModeVariable, ignoreCase: true, out mergeMode) || !Enum.IsDefined(mergeMode)))
        {
            error = $"{MergeModeVariable} '{mergeModeVariable}' is not recognised. Set it to Union, TokenOnly or DirectoryOnly, or unset it to use Union.";
            return false;
        }

        options = new ExplorerSampleOptions
        {
            Minimal = minimal,
            ExplorerRegion = region ?? SampleIdentities.EastRegion,
            SignInAs = string.Equals(signInAs, NoSignIn, StringComparison.OrdinalIgnoreCase) ? null : signInAs,
            StartPeerPaused = peerPaused,
            Entra = entra,
            GroupMergeMode = mergeMode,
            Ports = SamplePorts.Default.Offset(portOffset),
        };
        error = null;
        return true;
    }

    // Half-configuring Entra is rejected, so a partial configuration never
    // silently degrades to the static directory.
    private static bool TryReadEntra(
        Func<string, string?> environment,
        out SampleEntraDirectory? entra,
        [NotNullWhen(false)] out string? error)
    {
        var tenantId = environment(EntraTenantIdVariable);
        var clientId = environment(EntraClientIdVariable);
        var clientSecret = environment(EntraClientSecretVariable);
        var set = (string.IsNullOrWhiteSpace(tenantId) ? 0 : 1)
            + (string.IsNullOrWhiteSpace(clientId) ? 0 : 1)
            + (string.IsNullOrWhiteSpace(clientSecret) ? 0 : 1);

        entra = set == 3 ? new SampleEntraDirectory(tenantId!, clientId!, clientSecret!) : null;
        if (set is > 0 and < 3)
        {
            error = $"Entra directory mode is half-configured. Set all of {EntraTenantIdVariable}, {EntraClientIdVariable} and {EntraClientSecretVariable} to back the Access directory with your Entra tenant, or unset all three to use the built-in static directory. See samples/Explorer/README.md.";
            return false;
        }

        error = null;
        return true;
    }
}
````

## PeerLink.cs

````csharp
using Microsoft.AspNetCore.Http;

namespace Orleans.Lattice.Samples.Explorer;

/// <summary>
/// The link between the two regions, and the switch that pauses it so the
/// Replication area can show a link going <c>Lagging</c> and then <c>Stalled</c>.
/// </summary>
/// <remarks>
/// Each region's replication receiver runs behind <see cref="InvokeAsync"/>.
/// While the link is paused every cross-region replication call - live push,
/// snapshot bootstrap and saga control, in both directions - is refused with
/// <c>503 Service Unavailable</c>, which the sender's gRPC client reads as the
/// peer being unreachable. Nothing else on the endpoint is touched: the
/// Explorer's own facade calls keep working, so the console can watch the
/// backlog grow. Resuming lets the senders catch up from where they stopped.
/// </remarks>
internal sealed class PeerLink
{
    /// <summary>
    /// The path prefix every cross-region replication gRPC service shares
    /// (<c>LatticeReplication</c>, <c>LatticeRemoteSnapshot</c> and
    /// <c>LatticeSaga</c>). The replication <em>control and status</em> facades
    /// the Explorer calls live under <c>/orleans.lattice.api.replication</c> and
    /// are never matched.
    /// </summary>
    public const string ReplicationPathPrefix = "/orleans.lattice.replication.";

    private readonly Lock _gate = new();
    private volatile bool _paused;

    /// <summary>Raised after the link is paused or resumed, with the new state.</summary>
    public event Action<bool>? Changed;

    /// <summary>Whether cross-region replication is currently refused.</summary>
    public bool IsPaused => _paused;

    /// <summary>Pauses the link, or resumes it when it is paused.</summary>
    /// <returns>Whether the link is paused afterwards.</returns>
    public bool Toggle()
    {
        bool paused;
        lock (_gate)
        {
            paused = !_paused;
            _paused = paused;
        }

        Changed?.Invoke(paused);
        return paused;
    }

    /// <summary>Pauses the link; does nothing when it is already paused.</summary>
    /// <returns>Whether this call paused it.</returns>
    public bool Pause() => Set(paused: true);

    /// <summary>Resumes the link; does nothing when it is not paused.</summary>
    /// <returns>Whether this call resumed it.</returns>
    public bool Resume() => Set(paused: false);

    /// <summary>Whether <paramref name="path"/> is a cross-region replication call.</summary>
    /// <param name="path">The request path.</param>
    public static bool IsReplicationPath(PathString path) =>
        path.HasValue && path.Value!.StartsWith(ReplicationPathPrefix, StringComparison.Ordinal);

    /// <summary>
    /// Refuses a cross-region replication call while the link is paused, and
    /// passes every other request to <paramref name="next"/>.
    /// </summary>
    /// <param name="context">The request.</param>
    /// <param name="next">The rest of the pipeline.</param>
    public Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(next);

        if (_paused && IsReplicationPath(context.Request.Path))
        {
            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
            return Task.CompletedTask;
        }

        return next(context);
    }

    private bool Set(bool paused)
    {
        lock (_gate)
        {
            if (_paused == paused)
            {
                return false;
            }

            _paused = paused;
        }

        Changed?.Invoke(paused);
        return true;
    }
}
````

## ReplicationWriter.cs

````csharp
using System.Text;

namespace Orleans.Lattice.Samples.Explorer;

/// <summary>
/// A small background writer that keeps the replication links busy, so the
/// Replication area shows live contact, backlog and health. Each tick it
/// overwrites one key per target, cycling through a fixed key set: the trees
/// stay the same size however long the sample runs.
/// </summary>
internal sealed class ReplicationWriter : IAsyncDisposable
{
    private readonly IReadOnlyList<ReplicationWriterTarget> _targets;
    private readonly TimeSpan _interval;
    private readonly CancellationTokenSource _stop = new();
    private Task? _loop;
    private long _ticks;

    /// <summary>Creates a writer; nothing is written until <see cref="Start"/> or <see cref="WriteOnceAsync"/>.</summary>
    /// <param name="targets">The trees written, one key each per tick.</param>
    /// <param name="interval">The time between ticks.</param>
    public ReplicationWriter(IReadOnlyList<ReplicationWriterTarget> targets, TimeSpan interval)
    {
        ArgumentNullException.ThrowIfNull(targets);
        ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(interval, TimeSpan.Zero);
        _targets = targets;
        _interval = interval;
    }

    /// <summary>The number of ticks written so far.</summary>
    public long Ticks => Interlocked.Read(ref _ticks);

    /// <summary>The key a target writes on tick <paramref name="tick"/>: one of its <see cref="ReplicationWriterTarget.KeyCount"/> keys, in turn.</summary>
    /// <param name="target">The target.</param>
    /// <param name="tick">The zero-based tick.</param>
    public static string KeyFor(ReplicationWriterTarget target, long tick)
    {
        ArgumentNullException.ThrowIfNull(target);
        ArgumentOutOfRangeException.ThrowIfNegative(tick);
        return $"{target.KeyPrefix}{tick % target.KeyCount:D3}";
    }

    /// <summary>Starts writing on a timer. Calling it again does nothing.</summary>
    public void Start() => _loop ??= RunAsync(_stop.Token);

    /// <summary>Writes one tick: one key per target.</summary>
    /// <param name="cancellationToken">Cancels the tick.</param>
    public async Task WriteOnceAsync(CancellationToken cancellationToken = default)
    {
        var tick = Interlocked.Increment(ref _ticks) - 1;
        var value = Encoding.UTF8.GetBytes($"status-{tick}");

        // A trusted co-hosted writer, like the seeding: system-origin bypasses
        // the deny-by-default gate for this process's own writes.
        using var _ = LatticeSystemOrigin.Enter();
        for (var i = 0; i < _targets.Count; i++)
        {
            var target = _targets[i];
            await target.Tree.SetAsync(KeyFor(target, tick), value, cancellationToken).ConfigureAwait(false);
        }
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        await _stop.CancelAsync().ConfigureAwait(false);
        if (_loop is not null)
        {
            await _loop.ConfigureAwait(false);
        }

        _stop.Dispose();
    }

    private async Task RunAsync(CancellationToken cancellationToken)
    {
        using var timer = new PeriodicTimer(_interval);
        try
        {
            while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
            {
                try
                {
                    await WriteOnceAsync(cancellationToken).ConfigureAwait(false);
                }
                catch (Exception exception) when (exception is not OperationCanceledException)
                {
                    // A write that fails (a silo still starting or already
                    // stopping) is skipped; the next tick tries again.
                }
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // Stopped.
        }
    }
}
````

## ReplicationWriterTarget.cs

````csharp
namespace Orleans.Lattice.Samples.Explorer;

/// <summary>One tree the <see cref="ReplicationWriter"/> writes, and the fixed key set it cycles through.</summary>
/// <param name="Region">The region the tree is written in.</param>
/// <param name="Tree">The tree.</param>
/// <param name="KeyPrefix">The prefix of every key written.</param>
/// <param name="KeyCount">How many distinct keys are written, in turn; the tree never holds more.</param>
internal sealed record ReplicationWriterTarget(string Region, ILattice Tree, string KeyPrefix, int KeyCount)
{
    /// <summary>The key set's size, which must be positive.</summary>
    public int KeyCount { get; } = KeyCount > 0
        ? KeyCount
        : throw new ArgumentOutOfRangeException(nameof(KeyCount), KeyCount, "A writer target needs at least one key.");
}
````

## SampleBanner.cs

````csharp
using Orleans.Lattice.Membership;
using Orleans.Lattice.Samples.Explorer.TaskBoard;

namespace Orleans.Lattice.Samples.Explorer;

/// <summary>The console output a started sample prints: every URL, every sample identity and how to drive the estate.</summary>
internal static class SampleBanner
{
    /// <summary>Writes the banner for a started <paramref name="sample"/>.</summary>
    /// <param name="writer">Where the banner goes.</param>
    /// <param name="sample">The started sample.</param>
    /// <param name="startup">How long the start took.</param>
    public static void Write(TextWriter writer, ExplorerSample sample, TimeSpan startup)
    {
        ArgumentNullException.ThrowIfNull(writer);
        ArgumentNullException.ThrowIfNull(sample);

        var estate = sample.West is not null;
        writer.WriteLine();
        writer.WriteLine(estate
            ? "Orleans.Lattice Explorer sample - two regions, tenancy and replication in one process"
            : "Orleans.Lattice Explorer sample - one region (--minimal)");
        writer.WriteLine($"Started in {startup.TotalSeconds:0.0}s.");
        writer.WriteLine();

        writer.WriteLine("URLs");
        writer.WriteLine($"  Explorer console   {sample.Console.Url}   (connected to region '{sample.ConsoleRegion.Id}')");
        foreach (var region in sample.Regions)
        {
            writer.WriteLine($"  Region {region.Id,-11} gRPC {region.Plan.GrpcEndpoint}   (silo port {region.Plan.SiloPort}, gateway port {region.Plan.GatewayPort})");
        }

        writer.WriteLine();
        writer.WriteLine(sample.Console.SignInAs is { } signInAs
            ? $"Sample identities (any password signs in; the console signs in as '{signInAs}' automatically - {ExplorerSampleOptions.SignInAsSwitch} picks another)"
            : $"Sample identities (any password signs in; the console starts signed out - use its Sign in dialog)");
        writer.WriteLine($"  {SampleIdentities.Administrator,-15} bootstrap administrator and platform operator: every area");
        if (estate)
        {
            writer.WriteLine($"  {SampleIdentities.AcmeAdmin,-15} tenant admin of '{SampleIdentities.AcmeTenant}': the tenant-scoped view at /t/{SampleIdentities.AcmeTenant}");
            writer.WriteLine($"  {SampleIdentities.GlobexAdmin,-15} tenant admin of '{SampleIdentities.GlobexTenant}': the tenant-scoped view at /t/{SampleIdentities.GlobexTenant}");
        }

        writer.WriteLine($"  {SampleIdentities.Alice,-15} groups '{SampleIdentities.OperatorsGroup}' and '{SampleIdentities.TaskEditorsGroup}'");
        writer.WriteLine($"  {SampleIdentities.Bob,-15} group '{SampleIdentities.TaskViewersGroup}'");
        writer.WriteLine($"  {SampleIdentities.Carol,-15} group '{SampleIdentities.VisitorsGroup}'");
        writer.WriteLine(sample.Options.Entra is null
            ? "  Identity directory: static in-memory roster - the Access create form fails closed on any id not in it."
            : "  Identity directory: Microsoft Entra (Graph) - the Access picker and validated create use your tenant.");
        writer.WriteLine(sample.Options.GroupMergeMode == SubjectGroupMergeMode.TokenOnly
            ? "  Group-merge mode: TokenOnly - local membership is inert, so Access renders group editing disabled."
            : $"  Group-merge mode: {sample.Options.GroupMergeMode} - set LATTICE_MEMBERSHIP_MERGE_MODE=TokenOnly to see merge-mode gating.");

        writer.WriteLine();
        writer.WriteLine("Seeded");
        foreach (var line in sample.SeedLog)
        {
            writer.WriteLine("  " + line);
        }

        writer.WriteLine();
        writer.WriteLine($"The '{TaskBoardApp.Slug}' app is in Apps > Catalogue; see samples/Explorer/Apps/TaskBoard/README.md.");
        writer.WriteLine("Telemetry is hidden: it needs a Prometheus-compatible metrics backend, which this sample does not run.");
        if (estate)
        {
            writer.WriteLine($"A background writer updates '{SampleIdentities.FactoryFloorTree}' in both regions every {sample.Options.WriterInterval.TotalSeconds:0.#}s.");
            writer.WriteLine(sample.PeerLink.IsPaused
                ? "The peer link is PAUSED (--peer-paused): links go Lagging, then Stalled. Press P to resume it."
                : "Press P to pause the peer link (links go Lagging, then Stalled) and P again to resume it.");
            writer.WriteLine($"Run with {ExplorerSampleOptions.ExplorerRegionSwitch} {SampleIdentities.WestRegion} to point the console at the west region, or {ExplorerSampleOptions.MinimalSwitch} for one region.");
        }

        writer.WriteLine("Press Ctrl+C to stop.");
    }
}
````

## SampleCircuitDiagnostics.cs

````csharp
using Microsoft.AspNetCore.Components.Server;
using Microsoft.Extensions.Logging.Console;

namespace Orleans.Lattice.Samples.Explorer;

/// <summary>
/// Makes a console circuit fault diagnosable (issue #4011). The sample keeps its console
/// quiet - it clears every logging provider - so a circuit the framework terminated used
/// to leave nothing behind but the browser's "unhandled exception on the current circuit".
/// The console region now writes exactly one kind of record to the terminal: Blazor's own
/// Error record of an unhandled circuit exception, with its stack trace. In Development
/// the browser is also sent the detail (<see cref="CircuitOptions.DetailedErrors"/>).
/// </summary>
/// <remarks>
/// The record is the framework's, carrying the exception and the circuit id; the sample
/// adds no message of its own and logs no user input.
/// </remarks>
internal static class SampleCircuitDiagnostics
{
    /// <summary>The category prefix Blazor Server logs circuit faults under.</summary>
    public const string CircuitCategory = "Microsoft.AspNetCore.Components.Server.Circuits";

    /// <summary>Adds the terminal log of circuit faults, and nothing else, to a region's logging.</summary>
    /// <param name="logging">The console region's logging, with every provider already cleared.</param>
    public static void ConfigureLogging(ILoggingBuilder logging)
    {
        ArgumentNullException.ThrowIfNull(logging);
        logging.AddSimpleConsole(console => console.ColorBehavior = LoggerColorBehavior.Disabled);
        logging.SetMinimumLevel(LogLevel.None);
        logging.AddFilter(CircuitCategory, LogLevel.Error);
    }

    /// <summary>In Development, sends a circuit fault's detail to the browser as well.</summary>
    /// <param name="services">The console region's services.</param>
    /// <param name="environment">The console region's environment.</param>
    public static void ConfigureCircuits(IServiceCollection services, IHostEnvironment environment)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(environment);
        if (environment.IsDevelopment())
        {
            services.Configure<CircuitOptions>(circuit => circuit.DetailedErrors = true);
        }
    }
}
````

## SampleConsolePlan.cs

````csharp
namespace Orleans.Lattice.Samples.Explorer;

/// <summary>Where the Explorer console is served, which region's gRPC endpoint it dials and who it signs in as.</summary>
/// <param name="WebPort">The HTTP port the console is served on.</param>
/// <param name="Endpoint">The gRPC endpoint of the region the console connects to.</param>
/// <param name="ConfigPath">The console's persisted configuration file.</param>
/// <param name="SignInAs">The identity the console signs in as automatically, or <see langword="null"/> for none.</param>
internal sealed record SampleConsolePlan(int WebPort, Uri Endpoint, string ConfigPath, string? SignInAs = SampleIdentities.Administrator)
{
    /// <summary>The console's address.</summary>
    public Uri Url => new($"http://localhost:{WebPort}/");
}
````

## Remaining files

This sample is too large to inline in full. The remaining 29 file(s) are in the repository:

- [SampleEntraDirectory.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleEntraDirectory.cs)
- [SampleExplorerEnvironment.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleExplorerEnvironment.cs)
- [SampleIdentities.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleIdentities.cs)
- [SamplePorts.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SamplePorts.cs)
- [SampleRegion.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleRegion.cs)
- [SampleRegionPeer.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleRegionPeer.cs)
- [SampleRegionPlan.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleRegionPlan.cs)
- [SampleSeeder.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleSeeder.cs)
- [SampleSharedBackupSink.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/SampleSharedBackupSink.cs)
- [test/ConsoleCircuit.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/ConsoleCircuit.cs)
- [test/DemoBasicAuthenticatorTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/DemoBasicAuthenticatorTests.cs)
- [test/DirectorySpine.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/DirectorySpine.cs)
- [test/EstateSmokeTests.Apps.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/EstateSmokeTests.Apps.cs)
- [test/EstateSmokeTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/EstateSmokeTests.cs)
- [test/EstateSmokeTests.Tenancy.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/EstateSmokeTests.Tenancy.cs)
- [test/EstateSmokeTests.TenantSwitcher.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/EstateSmokeTests.TenantSwitcher.cs)
- [test/Explorer.Tests.csproj](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/Explorer.Tests.csproj)
- [test/ExplorerSampleOptionsTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/ExplorerSampleOptionsTests.cs)
- [test/MinimalSmokeTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/MinimalSmokeTests.cs)
- [test/PeerLinkTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/PeerLinkTests.cs)
- [test/ReplicationWriterTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/ReplicationWriterTests.cs)
- [test/SampleCircuitDiagnosticsTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SampleCircuitDiagnosticsTests.cs)
- [test/SampleExplorerEnvironmentTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SampleExplorerEnvironmentTests.cs)
- [test/SamplePortsTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SamplePortsTests.cs)
- [test/SampleRegionPlanTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SampleRegionPlanTests.cs)
- [test/SampleSeederTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SampleSeederTests.cs)
- [test/SampleSharedBackupSinkTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SampleSharedBackupSinkTests.cs)
- [test/SampleTestHost.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/SampleTestHost.cs)
- [test/TenancyRegionsSmokeTests.cs](https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/Explorer/test/TenancyRegionsSmokeTests.cs)
