Dataset
+@inject IServiceProvider Services
+
+@*
+ The page this project owes the sites it read and the servers it dials.
+
+ Spec §7.6 and §11 both require it, and neither of them calls it a feature: crediting the
+ directories a backfill read, publishing who is knocking and how to make them stop, and stating
+ the limits of what a measurement here proves are obligations already incurred by crawling.
+
+ It is prose and a short list, and it is deliberately the plainest page on the site — the whole
+ argument this project makes is that a fact stated with its provenance beats a fact presented
+ well, and the page making that argument would be a poor place to contradict it.
+*@
+
+@if (Plain)
+{
+ @PlainText.RenderAbout(Page)
+}
+else
+{
+ About — mu*index
+
+ About mu*index
+ @Page.Lede
+
+ @foreach (var section in Page.Sections)
+ {
+
+ @section.Heading
+
+ @foreach (var point in section.Points)
+ {
+ @point.Lead @point.Body
+ }
+
+ @if (section.Identity is { } identity)
+ {
+ @identity.Wording
+
+
+ - crawler
+ - @identity.Name
+ - contact
+ -
+ @identity.InfoUrl
+ @if (!identity.ContactConfigured)
+ {
+ @* Amber is the site's word for declared-but-unverified, and an address
+ nobody configured is exactly that: a value with nothing behind it. *@
+ — placeholder; this deployment set no contact address
+ }
+
+
+ }
+
+ @if (section.Sources.Count > 0)
+ {
+
+ }
+
+ @if (section.Licence is { } licence)
+ {
+
+ - code
+ - @licence.CodeLicence
+ - data, as this deployment serves it
+ -
+ @if (licence.DataLicenceUrl is { } url)
+ {
+ @licence.DataLicenceName
+ }
+ else
+ {
+ @licence.DataLicenceName
+ }
+
+ - credit as
+ - @licence.Attribution
+
+
+ @licence.Notice
+ }
+
+ }
+
+ read this page as plain text
+}
+
+@code {
+ [SupplyParameterFromQuery(Name = "plain")] private string? PlainFlag { get; set; }
+
+ private bool Plain => Truthy.Is(PlainFlag);
+
+ /*
+ The crawler's own options when the crawl loop is hosted in this process, and the defaults
+ when it is not. Asking the container rather than constructing one means the identity this
+ page publishes is the identity the probe was built from — a page that said MUINDEX-CRAWLER
+ while a deployment had configured something else would be describing the source tree instead
+ of the crawler, which is the failure mode of every claim on this site that nobody pinned.
+ */
+ private AboutPage Page => AboutPage.Build(
+ Services.GetService(typeof(ProbeOptions)) as ProbeOptions ?? new ProbeOptions(),
+ Dataset.Value);
+}
diff --git a/src/MUI.Web/Components/PlainText.cs b/src/MUI.Web/Components/PlainText.cs
index 89e054c..aa25301 100644
--- a/src/MUI.Web/Components/PlainText.cs
+++ b/src/MUI.Web/Components/PlainText.cs
@@ -350,6 +350,79 @@ public static string RenderArchive(IReadOnlyList entries, string?
return b.ToString();
}
+ ///
+ /// The about page. Prose, so the only thing the graphical version adds is the shape of it.
+ ///
+ ///
+ /// The attribution list is the part that has to survive here above all: it is what this project
+ /// owes the directories it read, and an acknowledgement a text browser cannot render is an
+ /// acknowledgement made to the layout rather than to anybody.
+ ///
+ public static string RenderAbout(AboutPage page)
+ {
+ var b = new StringBuilder();
+
+ b.AppendLine("ABOUT MU*INDEX");
+ b.AppendLine();
+ Wrap(b, page.Lede);
+
+ foreach (var section in page.Sections)
+ {
+ Heading(b, section.Heading);
+
+ foreach (var point in section.Points)
+ {
+ b.AppendLine();
+ Wrap(b, point.Sentence, " ");
+ }
+
+ if (section.Identity is { } identity)
+ {
+ b.AppendLine();
+ Wrap(b, identity.Wording, " ");
+ b.AppendLine();
+ Wrap(b, $"Crawler: {identity.Name}", " ");
+ Wrap(b, $"Contact: {identity.InfoUrl}", " ");
+
+ if (!identity.ContactConfigured)
+ {
+ Wrap(b, "This deployment has not set a contact address, so the one above is "
+ + "the built-in placeholder and answers nobody.", " ");
+ }
+ }
+
+ foreach (var source in section.Sources)
+ {
+ b.AppendLine();
+ b.AppendLine($" {source.Name} — {source.StatusWording}");
+ b.AppendLine($" {source.Url}");
+ Wrap(b, source.Note, " ");
+ }
+
+ if (section.Licence is { } licence)
+ {
+ b.AppendLine();
+ // Every one of these goes through the wrapper rather than being laid out in columns:
+ // a licence name and an attribution are both configuration, and a deployment that
+ // sets a long one must not push a line off the side of a text browser.
+ Wrap(b, $"Code: {licence.CodeLicence}", " ");
+ Wrap(b, $"Data: {licence.DataLicenceName}", " ");
+
+ if (licence.DataLicenceUrl is { } url)
+ {
+ Wrap(b, url, " ");
+ }
+
+ Wrap(b, "(what this deployment serves. The project's own answer is still open.)", " ");
+ Wrap(b, $"Credit as: {licence.Attribution}", " ");
+ b.AppendLine();
+ Wrap(b, licence.Notice, " ");
+ }
+ }
+
+ return b.ToString();
+ }
+
private static void Heading(StringBuilder b, string title)
{
b.AppendLine();
diff --git a/src/MUI.Web/wwwroot/app.css b/src/MUI.Web/wwwroot/app.css
index ceb4a40..940dd43 100644
--- a/src/MUI.Web/wwwroot/app.css
+++ b/src/MUI.Web/wwwroot/app.css
@@ -517,3 +517,21 @@ fieldset.facets .check { display: inline-flex; gap: 6px; align-items: center; fo
border-bottom: 1px solid var(--amber);
color: var(--text);
}
+
+/* ── about ─────────────────────────────────────────────────────────────────
+ Prose measure and nothing else. The one page whose whole job is to say what the rest of the site
+ can and cannot prove is the wrong place to spend the accent on decoration, so it carries no
+ colour of its own — the only tinted thing here is a contact address nobody configured, which is
+ amber for the same reason every other unverified value on this site is. */
+
+section.about { max-width: 68ch; }
+section.about h2 { margin-top: calc(var(--cpad) * 2.5); }
+
+dl.identity { font-size: 13px; margin: var(--cpad) 0; }
+dl.identity dt { margin-top: 8px; }
+dl.identity dd { margin: 2px 0 0; color: var(--dim); }
+
+ul.sources { list-style: none; margin: var(--cpad) 0 0; padding: 0; }
+ul.sources li { border-top: 1px solid var(--line); padding: var(--cpad) 0; }
+ul.sources .name { font-size: 15px; font-weight: 600; text-decoration: none; }
+ul.sources .source-head { display: flex; gap: var(--cpad); align-items: baseline; flex-wrap: wrap; }
diff --git a/tests/MUI.Web.Tests/AboutPageTests.cs b/tests/MUI.Web.Tests/AboutPageTests.cs
new file mode 100644
index 0000000..1792177
--- /dev/null
+++ b/tests/MUI.Web.Tests/AboutPageTests.cs
@@ -0,0 +1,356 @@
+using System.Text.RegularExpressions;
+
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+using MUI.Crawl;
+using MUI.Web.Api;
+using MUI.Web.Components;
+
+namespace MUI.Web.Tests;
+
+///
+/// The about page, which is an obligation this project incurred by crawling rather than a feature.
+///
+///
+///
+/// The load-bearing test here is not that the page renders: it is that the attribution list names
+/// every directory docs/import-sources.md says was read. That file is the record §7.6 leaves
+/// behind on main when the importer goes, and a credit that drifts from it is a credit that
+/// quietly drops whoever was added last.
+///
+///
+/// The rest assert sentences rather than markup, for the reason the whole plain surface exists: a
+/// limitation that only survives in the graphical page is a limitation stated to the layout.
+///
+///
+public class AboutPageTests
+{
+ private static AboutPage Page => AboutPage.Build(new ProbeOptions(), new DatasetLicenceOptions());
+
+ private static string Plain => PlainText.RenderAbout(Page);
+
+ [Test]
+ public async Task EveryDirectoryTheBackfillReadIsCreditedByNameAndByAddress()
+ {
+ // Parsed from the record rather than pasted from it. A source added to the document and not
+ // to the page is the failure this exists to catch, and a copied list cannot catch it.
+ var documented = DocumentedSources();
+
+ await Assert.That(documented).IsNotEmpty();
+ await Assert.That(Page.Sources).IsNotEmpty();
+
+ foreach (var (name, url) in documented)
+ {
+ await Assert.That(Plain).Contains(name);
+ await Assert.That(Plain).Contains(url);
+ }
+ }
+
+ [Test]
+ public async Task ASourceWeChoseNotToFetchIsCreditedAndSaidToBeUnread()
+ {
+ // MudVerse is written, tested and deliberately never run. Leaving it off would read as
+ // completeness; crediting it as read would be the same lie the other way round.
+ var mudverse = Page.Sources.Single(s => s.Name == "MudVerse");
+
+ await Assert.That(mudverse.State).IsEqualTo(ImportSourceState.Withheld);
+ await Assert.That(Plain).Contains("not read — waiting on permission");
+ }
+
+ [Test]
+ public async Task TheAttributionSaysAddressesOnlyAndSaysWhy()
+ {
+ await Assert.That(Render.Words(Plain)).Contains("We take addresses. Nothing else.");
+ await Assert.That(Plain).Contains("no player counts");
+ await Assert.That(Plain).Contains("no reachability history");
+ }
+
+ [Test]
+ public async Task TheCrawlOfMudStatsThatWentOutUnaskedIsOnThePage()
+ {
+ // The record matters more than the tidy version, and it matters most on the page that asks
+ // to be trusted about everything else.
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("143 of their pages");
+ await Assert.That(text).Contains("before anyone had written to them");
+ }
+
+ [Test]
+ public async Task TheArchiveGraceLimitationIsStatedInTheSpecsOwnTerms()
+ {
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("60-day floor on the day we discover it");
+ await Assert.That(text).Contains("reachable time we ourselves probed");
+ }
+
+ [Test]
+ public async Task TheReasonMsspCreatedEarnsNoGraceIsGiven()
+ {
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("do not credit MSSP CREATED");
+ await Assert.That(text).Contains("gameable");
+ }
+
+ [Test]
+ public async Task TheCrawlerIdentityIsReadOffTheProbeRatherThanWrittenOut()
+ {
+ // A name typed into the page is a description of the source tree. This one has to be the
+ // object the probe is constructed from, or a deployment that configures it gets a page that
+ // confidently names somebody else.
+ var probe = new ProbeOptions { TerminalTypes = ["EXAMPLE-CRAWLER"], InfoUrl = "https://example.test/bot" };
+ var text = PlainText.RenderAbout(AboutPage.Build(probe, new DatasetLicenceOptions()));
+
+ await Assert.That(text).Contains("EXAMPLE-CRAWLER");
+ await Assert.That(text).Contains("https://example.test/bot");
+ await Assert.That(text).DoesNotContain("MUINDEX-CRAWLER");
+ }
+
+ [Test]
+ public async Task TheCrawlerIsNotClaimedToAnnounceItselfWhileItCannot()
+ {
+ // TelnetNegotiationCore gives a client no way to set its terminal type, so nothing we
+ // configure reaches an admin's logs. Saying it does would be the ContactedMaintainer defect
+ // in a new place: a claim about our own behaviour, compiled in by whoever typed it.
+ var identity = Page.Sections.Single(s => s.Id == "crawler").Identity;
+
+ await Assert.That(identity).IsNotNull();
+ await Assert.That(identity!.Announced).IsFalse();
+ await Assert.That(Render.Words(Plain)).Contains("configured to call itself");
+ }
+
+ [Test]
+ public async Task AnUnconfiguredContactAddressIsMarkedAsThePlaceholderItIs()
+ {
+ // The built-in URL is on a domain nobody has chosen. Printed unmarked it would read as the
+ // way to reach us, which is the one thing this section exists to provide.
+ await Assert.That(Plain).Contains("built-in placeholder");
+
+ var configured = PlainText.RenderAbout(AboutPage.Build(
+ new ProbeOptions { InfoUrl = "https://example.test/crawler" }, new DatasetLicenceOptions()));
+
+ await Assert.That(configured).DoesNotContain("built-in placeholder");
+ }
+
+ [Test]
+ public async Task TheOptOutIsNotAdvertisedAsAutomatedWhileItIsNot()
+ {
+ // Neither the MSSP field nor the DNS TXT record the design describes exists in code. A page
+ // offering a switch that is wired to nothing is worse than a page admitting there is none.
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("no automated opt-out yet");
+ await Assert.That(text).Contains("Ask, and we stop.");
+ }
+
+ [Test]
+ public async Task ThePoliteAndSecurityFactsAboutAProbeSurvive()
+ {
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("CRAWL DELAY wins.");
+ await Assert.That(text).Contains("resolved before anything is dialled");
+ await Assert.That(text).Contains("globally routable");
+ }
+
+ [Test]
+ public async Task ThePermittedCommandIsReadOffTheProbeAndNotDescribedFromMemory()
+ {
+ // "It sends one WHO" is a claim about the probe. The probe publishes the list, so the page
+ // cannot understate it when a second command is ever added.
+ foreach (var command in TelnetProbe.PermittedCommands)
+ {
+ await Assert.That(Plain).Contains(command);
+ }
+ }
+
+ [Test]
+ public async Task TheMeasuredSpineSurvivesInWords()
+ {
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("Measured beats declared, and both are shown.");
+ await Assert.That(text).Contains("MSSP PLAYERS field");
+ await Assert.That(text).Contains("WHO or DOING read at the connect screen");
+ await Assert.That(text).Contains("unknown, never zero");
+ }
+
+ [Test]
+ public async Task ReachableIsExplainedAndNeverCalledUptimeExceptToRefuseTheWord()
+ {
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("Reachable, never uptime.");
+
+ // The word appears twice and both are refusals. Anything else would be the site's own
+ // vocabulary rule broken on the page that states it.
+ var uses = Regex.Matches(text, "uptime", RegexOptions.IgnoreCase).Count;
+ await Assert.That(uses).IsEqualTo(2);
+ await Assert.That(text).Contains("does not measure whether the game was up");
+ await Assert.That(text).Contains("nothing here measured it");
+ }
+
+ [Test]
+ public async Task TheThingsThisSiteWillNotDoAreStatedRatherThanImplied()
+ {
+ // The one page where these words are allowed to appear, because it is the page that says
+ // they are absent. PlainParityTests asserts the opposite about every other surface.
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("no votes, stars, ratings or recommendations");
+ await Assert.That(text).Contains("no forums, reviews, wikis, comments or player profiles");
+ await Assert.That(text).Contains("Player names are never persisted.");
+ await Assert.That(text).Contains("No absolute population figure is published.");
+ }
+
+ [Test]
+ public async Task TheDataLicenceIsPresentedAsUndecidedRatherThanAsSettled()
+ {
+ var text = Render.Words(Plain);
+
+ await Assert.That(text).Contains("The code is MIT.");
+ await Assert.That(text).Contains("licence for the data is an open question");
+ await Assert.That(text).Contains("has not been taken");
+
+ // What the deployment serves is still shown — a consumer needs the terms — but framed as
+ // this deployment's answer rather than as the project's.
+ await Assert.That(text).Contains("this deployment serves");
+ await Assert.That(text).Contains(new DatasetLicenceOptions().LicenceName);
+ }
+
+ [Test]
+ public async Task NoPlainLineIsWiderThanEightyColumns()
+ {
+ // The over-long lines rather than the first one, because a width failure is usually a whole
+ // block that was built without the wrapper and the count is the diagnosis.
+ var wide = Plain.Split('\n')
+ .Select(l => l.TrimEnd())
+ .Where(l => l.Length > PlainText.Columns)
+ .ToArray();
+
+ await Assert.That(wide).IsEmpty();
+ }
+
+ [Test]
+ public async Task TheGraphicalPageCarriesEverySentenceThePlainOneDoes()
+ {
+ // The parity that matters: both surfaces render one view model, so nothing can exist on one
+ // and not the other. Read off the rendered frame, because that is what a browser receives.
+ var markup = await RenderAboutAsync();
+ var text = Text(markup);
+
+ foreach (var section in Page.Sections)
+ {
+ await Assert.That(text).Contains(section.Heading);
+
+ foreach (var point in section.Points)
+ {
+ await Assert.That(text).Contains(Render.Words(point.Sentence));
+ }
+
+ foreach (var source in section.Sources)
+ {
+ await Assert.That(text).Contains(source.Name);
+ await Assert.That(text).Contains(Render.Words(source.Note));
+
+ // The address is the link here and a printed line there. Asserted against the
+ // markup rather than the reading text, because a graphical page that spelled its
+ // URLs out as well would be the plain page with worse typography.
+ await Assert.That(markup).Contains(source.Url);
+ }
+ }
+ }
+
+ [Test]
+ public async Task TheGraphicalPageIsReachableWithoutScriptingAndSaysHowToReadItPlainly()
+ {
+ var html = await RenderAboutAsync();
+
+ await Assert.That(html).DoesNotContain("