Skip to content

feat(console): MapGoldpathConsole() — the app serves its own console, Node-free - #83

Merged
omercelikdev merged 2 commits into
mainfrom
feat/u4-map-console
Jul 27, 2026
Merged

feat(console): MapGoldpathConsole() — the app serves its own console, Node-free#83
omercelikdev merged 2 commits into
mainfrom
feat/u4-map-console

Conversation

@omercelikdev

Copy link
Copy Markdown
Owner

U4 slice 3 (console RFC D1).

Goldpath.Console embeds the built single page and maps it on the management head, so an adopter runs the console the way they run everything else — one line in their app:

app.MapGoldpathConsole();                                  // this service only
app.MapGoldpathConsole(configure: console => console
    .AddService("payments", "https://payments.internal")
    .AddService("claims", "https://claims.internal"));     // one console, many services

Node is needed in exactly one place: Goldpath's CI, before the pack. The package refuses to pack without a built console rather than shipping a mapper that 404s in every adopter's face.

Decisions worth the words

  • The registry is configuration (AddService), not a JSON file someone must copy beside the dist. An adopter who already declares services in config should not declare them twice — and the same dist then works for everyone.
  • The same ops floor as the admin surfaces. The console drives them, so it inherits their guard, from the same shared source (H2).
  • A missing asset 404s instead of being answered with the page. A console that "loads" into a blank screen with a green status code is the worst failure it can have.
  • An unknown path 404s too — the console has no client-side routes yet, so serving the page there would answer 200 with a document whose relative assets resolve a directory too deep. Recorded as open-threads T9 with exactly what changes when routes arrive (the server injects <base href>, and these tests flip with it).

The serving logic is internal-but-tested, so its three outcomes — the page, a missing asset, a package built without a console — are proven deterministically instead of depending on whether the machine running the tests happened to build the dist.

Proof

The smoke now builds the dist, the test host maps the console, and Playwright drives that console rather than the dev server: its own assets load clean (the capability probes' legitimate 401/400s are scoped out), its registry is the three services the app configured, and both 404 shapes hold.

21/21 green. Console package tests 8; console unit 136; kit 64.

🤖 Generated with Claude Code

… Node-free

U4 slice 3 (console RFC D1). `Goldpath.Console` embeds the built single page and maps it
on the management head, so an adopter runs the console the way they run everything else:
one line in their app. Node is needed in exactly ONE place — Goldpath's CI, before the
pack — and the package refuses to pack without a built console rather than shipping a
mapper that 404s in every adopter's face.

Decisions worth the words:
- the REGISTRY is configuration (`AddService`), not a JSON file someone must copy beside
  the dist: an adopter who already declares services in config should not declare them
  twice, and the same dist then works everywhere;
- the console sits behind the SAME ops floor as the admin surfaces — it drives them, so
  it inherits their guard, from the same shared source (H2);
- a missing ASSET 404s instead of being answered with the page. A console that "loads"
  into a blank screen with a green status code is the worst failure it can have;
- an unknown PATH 404s too, because the console has no client-side routes yet: serving
  the page there would answer 200 with a document whose relative assets resolve a
  directory too deep. Recorded as open-threads T9, with what must change when routes
  arrive (the server injects a `<base href>`, and the tests flip with it).

The serving logic is internal-but-tested so its three outcomes — the page, a missing
asset, a package built without a console — are proven deterministically rather than
depending on whether the machine running the tests happened to build the dist.

Proof: the smoke now builds the dist, the test host maps the console, and Playwright
drives THAT console (not the dev server) — its assets load clean, its registry is the
three services the app configured, and the 404 shapes hold. 21/21 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@omercelikdev

Copy link
Copy Markdown
Owner Author

Review agent v1 — 2 finding(s). The human decides; hard-stop labels need explicit resolution.

  • R6 (high) packages/Goldpath.Console/GoldpathConsoleEndpoints.cs:116 — ContentTypeOf hand-rolls a MIME-type lookup table instead of composing Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider, which the project already has access to via the existing Microsoft.AspNetCore.App FrameworkReference.
    • evidence: CLAUDE.md invariant / ADR-0003: 'What Microsoft/Mediant provides is not rewritten — it is composed'; FileExtensionContentTypeProvider is the framework-provided primitive for exactly this extension→content-type mapping. · action: Replace the switch expression with new FileExtensionContentTypeProvider().TryGetContentType(path, out var contentType), falling back to application/octet-stream when it returns false.
  • R5 (medium) tests/Goldpath.Console.Tests/MapGoldpathConsoleTests.cs:55 — GetFromJsonAsync is called with default JsonSerializerOptions (case-sensitive property matching), but GoldpathConsoleEndpoints.cs:70 emits camelCase keys ("name", "adminBaseUrl") that must bind to the PascalCase record properties Name/AdminBaseUrl — under case-sensitive defaults these would deserialize as null rather than matching the asserted tuples.
    • evidence: System.Text.Json: JsonSerializerOptions.Default has PropertyNameCaseInsensitive = false; only JsonSerializerDefaults.Web sets it true. No such option is passed here. · action: Confirm these two registry tests actually go green in CI as claimed; if so note what makes the case-insensitive match happen, otherwise pass JsonSerializerOptions { PropertyNameCaseInsensitive = true } to GetFromJsonAsync.

Calibration: mark each finding accepted/dismissed in a reply — dismiss rate >40%/class revises that class (strategy §5).

ADR-0003 in one line: the hand-rolled extension→MIME switch is what
FileExtensionContentTypeProvider already does, and better — it knows the types a future
asset will use without anyone remembering to add them. Unknown extensions still fall back
to bytes rather than a guess.

Also notes, in the test that raised R5, WHY the camelCase payload binds to PascalCase
records without options: GetFromJsonAsync reads with JsonSerializerDefaults.Web, which is
case-insensitive. Checked rather than assumed.

21/21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@omercelikdev

Copy link
Copy Markdown
Owner Author

R6 accepted — ADR-0003 in one line. Replaced with FileExtensionContentTypeProvider, which also knows the types a future asset will use without anyone remembering to add them; unknown extensions still fall back to bytes rather than a guess.

R5 checked, then dismissed — the premise does not hold: HttpClient.GetFromJsonAsync reads with JsonSerializerDefaults.Web, which sets PropertyNameCaseInsensitive = true, so the camelCase payload binds to the PascalCase records without options. The tests were green for that reason, not by luck — and the test now says so in a comment, since the question was worth asking.

@omercelikdev
omercelikdev merged commit d323113 into main Jul 27, 2026
3 checks passed
@omercelikdev
omercelikdev deleted the feat/u4-map-console branch July 27, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant