Adds the AI-DLC inception record for deploying the CMS as a single .NET application on hosting where no server configuration is possible. The reverse-engineering artifacts were regenerated: the previous set predated the Master module, the Slave host, the solution reorganisation and single-host serving, all of which matter for deployment. Findings were verified by running the build, both test suites and the linter rather than inferred, which surfaced two facts the plan depends on: the frontend lint gate currently fails (5 errors), and two transitive packages carry high-severity advisories. Records 24 functional requirements, 32 traced decisions and a seven-unit decomposition whose ordering is load-bearing: durability work must land before the first automated deploy, or the very first deploy is the one that silently breaks master/slave trust. Two conflicts found while designing and carried into the units: - Both modules call AddDataProtection(), which runs after the host and would override a persistent key store while still passing any registration test. - The availability gate runs before authentication, so its admin bypass cannot read HttpContext.User. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
14 KiB
14 KiB
Code Quality Assessment
All figures below were measured during this analysis (2026-07-27) rather than inferred.
Build
dotnet build SlpModularCms.sln -c Release — succeeds: 0 errors, 50 warnings, ~27s.
Warning categories:
- NU1903 — known high-severity vulnerabilities in transitive packages (the majority of the 50). Confirmed by
dotnet list package --vulnerable --include-transitive:Microsoft.OpenApi2.0.0 — GHSA-v5pm-xwqc-g5wc (High)System.Security.Cryptography.Xml10.0.9 — GHSA-cvvh-rhrc-wg4q and four further advisories (High) Both arrive transitively (OpenAPI tooling; Data Protection's XML key handling). A CI gate ondotnet list package --vulnerablewould fail today until these are pinned to patched versions.
- NU1510 — redundant
PackageReferences that will not be pruned:Microsoft.Extensions.Logging.Abstractions(Core, Modules.Identity.Tests),Microsoft.Extensions.Hosting.Abstractions(Core.Tests). Cosmetic.
No C# compiler warnings — nullable reference types are respected throughout.
Test Coverage
Backend — all suites pass
| Suite | Tests | Result | Duration |
|---|---|---|---|
SlpModularCms.Core.Tests |
54 | ✅ all passed | 0.9s |
SlpModularCms.Modules.Identity.Tests |
37 | ✅ all passed | 1.0s |
SlpModularCms.Modules.Availability.Tests |
78 | ✅ all passed | 0.5s |
SlpModularCms.Modules.Master.Tests |
50 | ✅ all passed | 0.6s |
| Total | 219 | 0 failed, 0 skipped | ~3s |
Frontend — all suites pass
pnpm test (Vitest): 34 test files, 213 tests, all passed, ~39s. Every page, API hook and interactive component has a colocated test; MSW supplies request mocking per domain.
Coverage posture
- Unit tests: Good and genuinely broad — controllers, services, repositories and background services are all covered on the backend; pages, hooks and dialogs on the frontend.
- Integration tests: None. There is no
WebApplicationFactory-based suite, so the composed pipeline is never exercised end to end. The things that only exist in composition are therefore untested: middleware ordering, the availability gate's real interaction with static files, the/api/v1prefix convention, the two SPA fallbacks, CORS, rate limiting, and JWT validation against real configuration. - Contract tests: None for the master↔slave protocol. Both sides are unit-tested in isolation with mocks, so a change to one side's contract would not be caught.
- End-to-end tests: None.
- Coverage numbers:
coverlet.runsettingsis configured (excluding migrations,obj/, generated OpenAPI interceptors and[ExcludeFromCodeCoverage]members), and the frontend has atest:coveragescript with v8, but no threshold is enforced anywhere — nothing fails a build for dropping coverage.
Code Quality Indicators
- Backend linting: Nothing beyond compiler nullable warnings — no
.editorconfig, no analyzer package, no format check.dotnet format --verify-no-changesis not wired up anywhere. - Frontend linting: ESLint 10 with
typescript-eslint,react-hooksandreact-refreshplugins, plus Prettier withformat:check.pnpm run lintcurrently FAILS — see Technical Debt below. This is a blocking fact for any CI pipeline that runs lint as a gate. - Type checking:
tsc -bruns as part ofpnpm build, so type errors do fail the frontend build. - Code style: Consistent within each side. Backend uses XML doc comments on interfaces, entities and non-obvious services; several comments explain why rather than what (the
nonfileconstraint, the reasonbuild-productionis a separate job in the reference project, the master-gate bypass rationale). Frontend is Prettier-formatted with 4-space indent. - Comment language: Mixed Dutch and English in the C# codebase —
ModuleOrchestratorlogs and doc comments are Dutch, most newer code is English, and some user-facing strings are Dutch (AvailabilityMiddleware's 503 detail,SetupController's success message). Not a defect, but it means user-visible API messages are Dutch-only with no localisation path, while the frontend is fully i18n'd (NL/EN). - Documentation: Strong.
README.mdis thorough and current (including the single-host model and production setup);CLAUDE.md/AGENTS.md/.junie/guidelines.md/.github/copilot-instructions.mddocument the solution layout;aidlc-docs/holds the full AI-DLC history per feature. - Naming: Follows .NET and React conventions consistently.
- Reproducibility:
frontend/pnpm-lock.yamlexists and publish uses--frozen-lockfile. Nopackages.lock.jsonfor any .NET project, so NuGet restore is not locked.
Technical Debt
Blocking for CI as it stands
pnpm run lintfails: 5 errors, 1 warning. Any workflow that gates on lint will go red on the currentmaster:src/components/cms/AddCmsInstanceDialog.tsx:55—setStatecalled synchronously inside an effect (react-hooks/set-state-in-effect)src/components/users/InviteUserDialog.tsx:50— same rulesrc/components/users/InviteUserDialog.tsx:54— variable accessed before declarationsrc/pages/SettingsPage.tsx:40— samesetState-in-effect rulesrc/components/cms/SetStatusDialog.tsx:32—react-refresh/only-export-components: a non-component export shares the filesrc/components/cms/SetStatusDialog.tsx:72— warning: "Compilation Skipped: Use of incompatible library" Note the tests all pass regardless — these are lint-rule violations, not observed runtime failures.
- Two high-severity transitive vulnerabilities (
Microsoft.OpenApi2.0.0,System.Security.Cryptography.Xml10.0.9). A vulnerability gate cannot be switched on until these are addressed.
Deployment and operations gaps
- No CI/CD whatsoever — no
.gitea/workflows/, no build/test/deploy automation. Every deployment is manual today. - No health-check endpoint. There is nothing to point uptime monitoring at, and no existing endpoint can stand in:
AvailabilityandSystem/capabilitiesare CMS domain functionality (product on/off state and loaded-module reporting, both also serving the master↔slave protocol), not health signals. A dedicated health check — outside/api/v1domain routing and outside the availability gate, reporting infrastructure liveness such as process up, database reachable and migrations applied — has to be built. It is in scope for thegitea-deployment-workflowfeature because the framework supplies it almost for free:AddHealthChecks()+MapHealthChecks("/health")require no package, and a database probe costs onlyMicrosoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore10.0.9 plus.AddDbContextCheck<ApplicationDbContext>()./healthmust be added toAvailabilityMiddleware._bypassPrefixes, or the gate will return 503 for it on a disabled instance. Seeapi-documentation.md§ Observability endpoints. - No observability. No Sentry, no structured logging, no analytics, no metrics or tracing. Logging is the console provider at
Warninglevel in the production baseline — which means production would emit almost nothing useful. - Data Protection has no persistent key ring. Both
ApiKeyProtectorandMasterApiKeyProtectoruse the default file-system store. A redeploy or app-pool recycle that discards the key folder makes every stored slave API key permanently unreadable, silently breaking master↔slave communication until instances are re-added. Already flagged inREADME.md; still unaddressed in code. - No forwarded-headers middleware, while
UseHttpsRedirection()runs early. Behind a hosting provider's TLS-terminating proxy the app sees plain HTTP, which can cause redirect loops or wrong scheme in generated URLs. There is noUseForwardedHeadersand noASPNETCORE_FORWARDEDHEADERS_ENABLEDguidance. - The admin SPA bundle is environment-specific.
frontend/src/lib/config.tsrequiresVITE_API_BASE_URLas an absolute URL, so a test build and a production build cannot be the same artifact — even though in the single-host model the API is same-origin with the SPA and a relative base would work. This forces "build twice" in CI (exactly the pattern the reference project had to adopt forVITE_APP_ENV), or a small code change to default to same-origin. dotnet publishrequires Node and pnpm because of theBuildAndCopyAdminFrontendtarget. Convenient locally; a hard constraint on any build agent, and it couples backend publish time to frontend install/build time.ApplicationDbContextmigrations are never applied automatically while the two module contexts are. This asymmetry means a fresh deployment silently starts with no Identity tables until someone runsdotnet ef database update, and there is no migration step in any pipeline (because there is no pipeline).- No
Testenvironment configuration. Onlyappsettings.jsonandappsettings.Development.jsonexist; there is noappsettings.Test.jsonand no definedASPNETCORE_ENVIRONMENTvalue for the test environment, even though three environments are in scope. - The production
appsettings.jsonships placeholder secrets (<secure-long-random-secret-key-from-env>,<production-db-host>). Safe — they are not real credentials, and env vars are expected to override them — but startup fails confusingly rather than clearly if an env var is missed, sinceJwtSettings:Secretis present-but-nonsense rather than absent.
Design-level debt
- The availability gate does not cover the public website. Static files are served before
orchestrator.UseModules(app)installsAvailabilityMiddleware, so disabling an instance blocks the API and admin routes but still serveswwwroot/index.htmland its assets. Whether that is intended is a product decision, but it is currently implicit rather than documented, and it changes what "we disabled that customer" actually means. AvailabilityController.UpdateStatuscasts the injectedIAvailabilityServicetoPersistentAvailabilityServiceand returns400if the cast fails — the controller depends on a concrete implementation. Carried over from the previous assessment; still present.- Module load failures are swallowed.
ModuleOrchestratorlogs and continues when an assembly cannot be loaded or a module cannot be instantiated. A deployment that ships a broken or missing module DLL starts up "successfully" with reduced capability rather than failing fast — hard to detect without monitoring, and directly relevant to trusting a deployment. AvailabilityMiddleware.IsAdminBypassreads the JWT without validating its signature (JwtSecurityTokenHandler.ReadJwtToken), so anyone can craft an unsigned token carrying anOwnerrole claim and bypass the availability gate. Requests still fail authentication at protected endpoints afterwards, so this is not a privilege escalation — but it does mean the gate is bypassable by an unauthenticated caller, including on a master-disabled instance.- Inconsistent persistence style:
Modules.MasterandModules.Availabilityuse the repository pattern;Core's identity services useApplicationDbContextand Identity managers directly. Recognised divide between older and newer code. Microsoft.Extensions.Http.Resilience9.6.0 onnet10.0targets — the only dependency out of step with the otherwise uniform 10.0.x line.- API error and status messages are Dutch-only, with no localisation mechanism on the backend, while the frontend is fully bilingual.
Patterns and Anti-patterns
Good Patterns
- Module/plugin architecture with reflection-based discovery — capability is a property of what is deployed, which makes the master/slave distinction a packaging concern rather than a configuration flag.
- Push and pull status synchronisation with fail-open — the master↔slave design assumes messages get lost and instances restart, and it self-heals in both directions without a broker. The fail-open rule is the right default for a commercial kill-switch.
UpdateStatusResult { success, slaveContactSuccess }— honestly reports partial success instead of collapsing two different outcomes into one boolean.- JWT with refresh-token rotation, access token in memory only, refresh cookie httpOnly and path-scoped to
/api/v1/auth. - Uniform RFC 9457
ProblemDetailsvia a global handler, mirrored by a typedProblemDetailsErrorin the frontend client. - Centralised route prefixing (
ApiPrefixConvention) rather than repeatingapi/v1in every controller. - The
nonfileroute constraint on both SPA fallbacks — missing assets still 404 instead of being handed an HTML page, which is a genuinely easy mistake to make. - Options pattern used consistently for all four configuration sections.
- Resilience pipeline on outbound master→slave calls with jittered exponential backoff.
- High, real test coverage with fast suites (219 backend tests in ~3s) and MSW-based frontend tests that avoid brittle mocking.
- Comments that explain rationale, not mechanics — several of the trickiest decisions in the codebase are documented at the point of the decision.
- Three-file appsettings pattern with
appsettings.local.jsongitignored and no real secrets committed.
Anti-patterns
- Concrete-type cast in
AvailabilityController.UpdateStatus(item 14). - Unvalidated JWT parsing in the availability gate's admin bypass (item 16).
- Silent module-load failure (item 15).
- Build-time environment coupling in the frontend config, forcing per-environment bundles (item 8).
- Backend project reference used purely as a deployment mechanism for module DLLs — it works and is documented, but the compile-time dependency does not reflect an actual code dependency.
- Asymmetric migration strategy across the three
DbContexttypes (item 10). - Mixed-language comments and Dutch-only user-facing API strings (items 19 and the note above).