--- status: Accepted date: 2026-07-16 applyTo: - "Fuchs/Controllers/IntranetController.Admin.cs" - "Fuchs/Services/SystemStatusService.cs" - "Fuchs/Services/ISystemStatusService.cs" - "Fuchs/Services/SystemStatusModels.cs" - "Fuchs/js/intranet/modules/fis.admin*.js" - "Fuchs/js/intranet/modules/fis.admin.scss" supersededBy: "" --- # 0011 — Admin module gated on `fds_sys` > 4 ## Context Operators needed an in-app view of the running deployment's health: which host it runs on, whether SQL Server / Azure Key Vault / Azure Blob Storage / the MFR ERP are reachable, how the email service is configured (including the dev/test `Fuchs:Email:OverrideRecipient` redirect), and a way to send a test email. The `StartupSelfTestService` already probes most of these once at startup and writes the result to the log — but that is invisible to a logged-in operator and cannot be re-run on demand. This is privileged, infrastructure-revealing information (server names, account names, connectivity state) that must not be exposed to ordinary users. ## Decision - Add an **Admin** module (`admin`) alongside the existing invoice/reminder/report/banking modules, following the same frontend module contract (`init:admin` → `admin/auth` returns `{ manage }` → lazy-load `/web/fis.admin.de.js` + `/web/fis.admin.css` → `init2()`). - Access is gated on the caller's **`fds_sys` module authorization being strictly greater than 4** (`fis_getModuleAuth('fds_sys', authuser) > 4`). The menu button is only rendered, and the module script only fetched, for such users; **every** Admin data endpoint additionally re-checks the level server-side (defense in depth) and returns 401 otherwise. `admin/auth` is the sole endpoint that answers for unauthorized users too (it returns `manage: 0`), so the frontend can decide whether to render the module at all. - Backend diagnostics live in a new DI-registered `ISystemStatusService` (scoped). It exposes a passive `GetInfo()` snapshot and live, never-throwing connectivity probes (`database`, `keyvault`, `blob`, `mfr`) plus `SendTestEmailAsync`. The test email goes through the normal `IComService` pipeline, so the `OverrideRecipient` safety net applies exactly as for any other outbound mail. - The service never returns secrets — only presence flags and non-sensitive values (server/catalog/login name, storage account name, error text). Responses are serialized camelCase to match the frontend's lowercase convention. ## Consequences - `fds_sys` is now a security-relevant authorization key: granting it a value > 4 exposes infrastructure status and the ability to send test emails. Provision it deliberately. - `SystemStatusService` is intentionally **separate** from `StartupSelfTestService` rather than a shared refactor: the startup service's probe methods are `protected virtual` and its tests override them, so folding both onto one probe surface would have broken that contract. The two therefore duplicate a little probe logic (Key Vault secret read, MFR `GetEntities`, SQL `SELECT`); keep them behaviourally aligned when either changes. - New probes (or new status facts) belong in `SystemStatusService` behind the same `SystemProbeResult` / `SystemInfoSnapshot` shapes; add the component id to `SystemStatusService.Components` and a card in `fis.admin.js`. - Blob connectivity is checked via a new `IBlobStorageService.CheckConnectivityAsync` (account-info request); it too never throws. ## Alternatives considered - **Reuse `StartupSelfTestService` directly.** Rejected — see Consequences; its virtual/overridden probe surface is owned by its tests, and it is a one-shot `BackgroundService`, not a request-scoped query service. - **Gate only in the frontend (hide the menu button).** Rejected — the endpoints would still be reachable by crafting the POST. Server-side enforcement on every endpoint is required. - **A separate `fds_admin`/new module-auth key.** Rejected — `fds_sys` already models system-level privilege; reusing it avoids a parallel permission to provision.