You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Migrate the DevTools plugin frontend (src/Plugins/Solutions/DevTools/UI) from single embedded HTML file with inline JavaScript to typed, modular TypeScript implementation compiled into single embedded HTML resource.
Goal
The DevTools plugin (Swagger UI for REST + web UI for browsing and invoking gRPC methods) serves its frontend as one embedded resource. The migration should produce maintainable, type safe frontend with clean separation between rendering, domain logic, state, and the HTTP boundary, while preserving the single file embedded resource serving model and the runtime {{pathBase}} substitution performed by middleware.
Problem
The current UI/ui.html is ~330 line file mixing inline JavaScript, HTML, and CSS. The logic is monolith (state, string built DOM via innerHTML, manual esc() injection, inline fetch calls). There is no toolchain: no type checking, no bundling, and no way to reuse or unit test pieces of the frontend. It also works only accidentally prettyPrint receives an object instead of string and renders [object Object] in the Request JSON field.
DevToolsMiddleware.LoadEmbeddedPage() locates the resource by DevTools.UI.ui.html and replaces {{pathBase}} with the configured GrpcUiPathBase.
Requirements
Frontend must be authored in strict TypeScript (strict: true, tsc --noEmit clean).
A build step must compile the UI with esbuild and inline it into single dist/ui.html (no separate JS bundle).
dist/ui.html must be the only frontend artifact DevTools.csproj embeds it as EmbeddedResource with LogicalName="DevTools.UI.ui.html".
Rendering must use DOM APIs (createElement, textContent, classList) no innerHTML interpolation, no manual HTML escaping.
The {{pathBase}} placeholder must remain in the HTML (not in the bundle) and be the middleware's only insertion point.
Rendering, domain logic, and state must be separated so pure parts (schema.ts) are testable without browser.
The composition root must consume the GrpcApi interface API transport must be hidden behind HttpGrpcApi.
The Docker build must compile the UI in node:22 stage before publishing the plugin.
Taskfile.ymlbuild must run build-ui before dotnet build a watch-ui task must rebuild on changes.
Backward Compatibility
Resource name DevTools.UI.ui.html must stay unchanged DevToolsMiddleware.cs load path requires no changes.
{{pathBase}} substitution semantics must stay unchanged.
DOM structure and CSS classes must be preserved so the UI looks identical.
Plugin loading, manifest (authkit.devtools), and serving paths (/grpc-ui, /devtools) must stay unchanged.
Validation
npm run typecheck 0 errors.
npm run build produces dist/ui.html with inlined bundle, no leftover /*__BUNDLE__*/ marker, {{pathBase}} intact.
dotnet build DevTools.csproj succeeds DevTools.UI.ui.html present in the plugin DLL.
Runtime check loads the published DLL and reads the embedded resource (placeholder + bundle present).
docker build succeeds end to end (UI stage -> publish stage).
Acceptance Criteria
Functional
/grpc-ui renders the catalog: each discovered service listed with package name and method count first service expanded by default.
Selecting method highlights it in the side panel and renders Request schema, Response schema, Metadata headers, and Request JSON cards.
Fill defaults populates the Request JSON from the schema (correct defaults per field type: "", 0, false, [], {}, first enum value) recursive/nested and repeated fields handled.
Format JSON pretty prints the textarea invalid JSON is left untouched.
Invoke sends unary request to /api/invoke, shows status code + elapsed ms, renders response body (pretty printed), errors, and trailers; streaming methods get disabled Invoke button.
Headers typed in the Metadata card persist across method switches via sessionStorage.
Errors (catalog fetch failure, HTTP error, non JSON response) are surfaced in the UI, never as blank page or unhandled console crash.
Build & packaging
npm run typecheck exits 0 (strict TypeScript).
npm run build produces single dist/ui.html whose only script entries are the {{pathBase}} init snippet and the inlined IIFE bundle no /*__BUNDLE__*/ marker, no inline <script src>.
dotnet build succeeds and the published DevTools.dll contains exactly one frontend resource: DevTools.UI.ui.html.
Loading the published DLL at runtime yields the embedded HTML with the {{pathBase}} placeholder intact.
From a clean clone (npm ci && npm run build && dotnet publish) the plugin publishes without committing node_modules/ or dist/.
docker build completes through the ui and publish stages and serves on startup.
Structure & boundaries
The composition root references HttpGrpcApi only once and otherwise consumes the GrpcApi interface no other module calls fetch.
No innerHTML assignments and no string built DOM anywhere (only textContent/classList/createElement).
No HTTP/transport concern lives in schema.ts, store.ts, or render.ts.
No runtime type casts outside dom.tsgetById.
Non regression
Swagger UI and the landing page (/devtools) still serve unchanged.
Resource name and {{pathBase}} behavior unchanged; DevToolsMiddleware.cs load path untouched.
Non Goals
No framework (React/Vue/Svelte) and no bundler beyond esbuild.
No CSS refactor existing theme and classes retained.
No change to the .NET plugin contract or middleware router.
No frontend test framework type safety enforced via tsc.
Summary
Migrate the DevTools plugin frontend (
src/Plugins/Solutions/DevTools/UI) from single embedded HTML file with inline JavaScript to typed, modular TypeScript implementation compiled into single embedded HTML resource.Goal
The DevTools plugin (Swagger UI for REST + web UI for browsing and invoking gRPC methods) serves its frontend as one embedded resource. The migration should produce maintainable, type safe frontend with clean separation between rendering, domain logic, state, and the HTTP boundary, while preserving the single file embedded resource serving model and the runtime
{{pathBase}}substitution performed by middleware.Problem
The current
UI/ui.htmlis ~330 line file mixing inline JavaScript, HTML, and CSS. The logic is monolith (state, string built DOM viainnerHTML, manualesc()injection, inlinefetchcalls). There is no toolchain: no type checking, no bundling, and no way to reuse or unit test pieces of the frontend. It also works only accidentallyprettyPrintreceives an object instead of string and renders[object Object]in the Request JSON field.Scope
Architecture
flowchart TD TS["UI/src/*.ts (TS modules)"] -->|esbuild bundle| JS["dist/bundle.js"] TEMPLATE["UI/template.html (HTML+CSS)"] -->|build.mjs inlines bundle| UI["dist/ui.html"] UI -->|EmbeddedResource| CSPROJ["DevTools.csproj"] CSPROJ -->|"LogicalName DevTools.UI.ui.html"| DLL["DevTools.dll"] DLL -->|"LoadEmbeddedPage + Replace {{pathBase}}"| MID["DevToolsMiddleware"] MID -->|serves| BROWSER["Browser"] classDef ts fill:#ffebee,stroke:#c62839,color:#7b1f2e classDef build fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef cs fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 classDef serve fill:#fff3e0,stroke:#e65100,color:#bf360c class TS,TEMPLATE ts class JS,UI build class CSPROJ,DLL cs class MID,BROWSER serveFrontend module boundaries:
flowchart TD APP["app.ts (composition root)"] -->|GrpcApi interface| API["api.ts HttpGrpcApi"] APP --> CTRL["controller.ts createInvokeAction"] CTRL --> RENDER["render.ts renderServiceList / renderMethodDetail"] RENDER -->|pure functions| SCHEMA["schema.ts domain logic"] CTRL --> SCHEMA APP --> STORE["store.ts InMemoryDevToolsStore"] APP --> STORAGE["storage.ts sessionStorage headers"] RENDER --> DOM["dom.ts el / getById"] API -->|fetch| HTTP["/api/services /api/invoke"] classDef root fill:#ede7f6,stroke:#4527a0,color:#311b92 classDef infra fill:#e1f5fe,stroke:#01579b,color:#0d47a1 classDef domain fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 class APP root class API,DOM,STORAGE,HTTP infra class CTRL,RENDER,SCHEMA,STORE domainProposed Contract
The frontend boundary must be typed gRPC client the composition root (
app.ts) depends on theGrpcApiinterface and never touchesfetchdirectly:The embedded resource must keep its runtime placeholder so the middleware can inject the serving path without rebuild:
DevToolsMiddleware.LoadEmbeddedPage()locates the resource byDevTools.UI.ui.htmland replaces{{pathBase}}with the configuredGrpcUiPathBase.Requirements
strict: true,tsc --noEmitclean).esbuildand inline it into singledist/ui.html(no separate JS bundle).dist/ui.htmlmust be the only frontend artifactDevTools.csprojembeds it asEmbeddedResourcewithLogicalName="DevTools.UI.ui.html".createElement,textContent,classList) noinnerHTMLinterpolation, no manual HTML escaping.{{pathBase}}placeholder must remain in the HTML (not in the bundle) and be the middleware's only insertion point.schema.ts) are testable without browser.GrpcApiinterface API transport must be hidden behindHttpGrpcApi.node:22stage before publishing the plugin.Taskfile.ymlbuildmust runbuild-uibeforedotnet buildawatch-uitask must rebuild on changes.Backward Compatibility
DevTools.UI.ui.htmlmust stay unchangedDevToolsMiddleware.csload path requires no changes.{{pathBase}}substitution semantics must stay unchanged.authkit.devtools), and serving paths (/grpc-ui,/devtools) must stay unchanged.Validation
npm run typecheck0 errors.npm run buildproducesdist/ui.htmlwith inlined bundle, no leftover/*__BUNDLE__*/marker,{{pathBase}}intact.dotnet build DevTools.csprojsucceedsDevTools.UI.ui.htmlpresent in the plugin DLL.docker buildsucceeds end to end (UI stage -> publish stage).Acceptance Criteria
Functional
/grpc-uirenders the catalog: each discovered service listed with package name and method count first service expanded by default.Fill defaultspopulates the Request JSON from the schema (correct defaults per field type:"",0,false,[],{}, first enum value) recursive/nested and repeated fields handled.Format JSONpretty prints the textarea invalid JSON is left untouched.Invokesends unary request to/api/invoke, shows status code + elapsed ms, renders response body (pretty printed), errors, and trailers; streaming methods get disabled Invoke button.sessionStorage.Build & packaging
npm run typecheckexits 0 (strict TypeScript).npm run buildproduces singledist/ui.htmlwhose only script entries are the{{pathBase}}init snippet and the inlined IIFE bundle no/*__BUNDLE__*/marker, no inline<script src>.dotnet buildsucceeds and the publishedDevTools.dllcontains exactly one frontend resource:DevTools.UI.ui.html.{{pathBase}}placeholder intact.npm ci && npm run build && dotnet publish) the plugin publishes without committingnode_modules/ordist/.docker buildcompletes through theuiandpublishstages and serves on startup.Structure & boundaries
HttpGrpcApionly once and otherwise consumes theGrpcApiinterface no other module callsfetch.innerHTMLassignments and no string built DOM anywhere (onlytextContent/classList/createElement).schema.ts,store.ts, orrender.ts.dom.tsgetById.Non regression
/devtools) still serve unchanged.{{pathBase}}behavior unchanged;DevToolsMiddleware.csload path untouched.Non Goals
esbuild.tsc.