146 lines
11 KiB
Markdown
146 lines
11 KiB
Markdown
---
|
|
status: Active
|
|
lastUpdated: 2026-09-15
|
|
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"
|
|
- "Fuchs/js/intranet/fis_main_menu.js"
|
|
relatedDecisions:
|
|
- "0011-admin-module-system-status.md"
|
|
- "0015-split-admin-user-management-module.md"
|
|
---
|
|
|
|
# Admin / System-Status module
|
|
|
|
## Summary
|
|
The **Admin** module gives a privileged operator a live, in-app view of the running
|
|
deployment's configuration and health, plus a test-email tool. It answers "which host am I
|
|
on, can this instance reach SQL Server / Key Vault / Blob Storage / the MFR ERP, how is
|
|
email wired up (including the OverrideRecipient redirect), and does sending actually work".
|
|
Access is restricted to users whose `fds_sys` module authorization is greater than 4.
|
|
|
|
## How it works
|
|
|
|
### Authorization (two layers)
|
|
1. **Menu visibility** — at page load `fis_main_menu.js#addAdminMenuButtonsIfAuthorized` calls
|
|
`$fis.getAuth('fds_sys')` once (shared with the separate `usr` user-management module, see
|
|
[0015](../Decisions/0015-split-admin-user-management-module.md)); only when the level is > 4
|
|
does it push a single "Administration" dropdown entry into `$ocms.ocmsmenu` — containing this
|
|
module's "Systemüberwachung" (`init:admin`) item alongside `usr`'s "Benutzerverwaltung"
|
|
(`init:usr`) item — and re-render `#mainmenu`. Users below the threshold never see the
|
|
dropdown and never fetch either module's script. Within the dropdown, "Systemüberwachung"
|
|
(like "Spezialfunktionen") is a `shift-only` item: it is listed only when the dropdown is
|
|
opened with **Shift** held (`$fis.bindShiftOnlyMenuItems`); the `/admin` deep link is
|
|
unaffected. This is a convenience/accident guard, not an authorization level.
|
|
2. **Server-side gate** — every `/do/admin/*` endpoint resolves
|
|
`fis_getModuleAuth('fds_sys', authuser)` and returns 401 unless it is > 4. The only
|
|
exception is `admin/auth`, which returns `{ manage: 0 }` for unauthorized users so the
|
|
frontend can cleanly decline to render. This is defense in depth: hiding the button is
|
|
not a security control on its own.
|
|
|
|
### Request flow
|
|
```
|
|
click "Administration" → $ocms.init('admin')
|
|
POST /do/admin/auth → { manage, level } (manage>0 ⇒ authorized)
|
|
load /web/fis.admin.de.js + /web/fis.admin.css
|
|
$ocms.admin.init2() → init3() renders the page
|
|
POST /do/admin/status → { info, probes } full snapshot + all probes
|
|
per-card "Aktualisieren" → POST /do/admin/probe/<component> → { probe }
|
|
"Test-E-Mail senden" → POST /do/admin/testmail (to/subject/body/attachpdf) → { result }
|
|
```
|
|
|
|
### Backend (`ISystemStatusService` / `SystemStatusService`, scoped)
|
|
- `GetInfo()` — passive snapshot, no network calls: host (machine/OS/framework/environment/
|
|
test-deployment/uptime), database (server/catalog/login parsed from the connection string —
|
|
**never the password**), email (mailer enabled/base-url/account/server-id/token-present +
|
|
OverrideRecipient), blob (enabled/configured/containers), MFR (host/creds-present/sync),
|
|
Key Vault (vault-uri/app-prefix/managed-secret-count/client-registered).
|
|
- Probes (`database`, `email`, `keyvault`, `blob`, `mfr`) each return a `SystemProbeResult`
|
|
(`status` = `ok`/`error`/`disabled`/`unconfigured`, `ok`, `message`, `detail`, `durationMs`,
|
|
optional `metrics`). They never throw — failures are captured in the result. `database` runs
|
|
`SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME()`; `email` calls
|
|
`IComService.VerifyCredentialsAsync` (GET `{BaseUrl}/api/mailer/verify` with the configured
|
|
Basic-auth account/token — see the Mailer API contract) and only turns `ok` when the Mailer API
|
|
itself confirms the credentials (`ok:true`); when `ProcessWebComSettings.ServerId` is configured
|
|
it is sent along as `&serverid=...` so the API also confirms this server belongs to the account
|
|
(an account/token that is valid but for a server the account doesn't own comes back `ok:false`
|
|
with the mismatched `serverid` echoed, surfaced in `detail`); a configured-but-wrong token
|
|
surfaces as `error`, never as a false-positive `ok` — the `mailerEnabled`/`tokenConfigured` flags
|
|
in `GetInfo()` describe configuration only and were never proof the credentials actually work;
|
|
`keyvault` reads
|
|
the first managed secret via the DI `SecretClient`; `blob` calls
|
|
`IBlobStorageService.CheckConnectivityAsync` (account-info request **plus a per-container blob
|
|
count** for the invoice/reminder containers, surfaced as `metrics` and totalled in the message);
|
|
`mfr` calls `IMfrClientFactory.Create().GetEntities()`. The generic `metrics` (label/value pairs)
|
|
is how a probe reports extra facts for display — the blob probe uses it for the file counts.
|
|
- `SendTestEmailAsync(to, subject, body, attachPdf)` HTML-encodes the body and sends via
|
|
`IComService`, so the `Fuchs:Email:OverrideRecipient` redirect applies exactly as for any other
|
|
mail; the result reports the requested recipient and, when active, the override target.
|
|
With `attachPdf` (dialog select "PDF-Anhang mitsenden", default *Ja*; absent form value ⇒ on)
|
|
a one-page letter PDF (`Fuchs-Intranet-Testmail.pdf`) is rendered in memory via
|
|
`IPdfService.WriteLetterAsync` + `DocToPdfBytes` — standard Fuchs letterhead, the recipient in
|
|
the address window, `Datum`/`Umgebung`/`Nummer=admin_test` in the admin block, the mail's own
|
|
subject and body as the letter text; no DB or stored document is touched. This exercises the
|
|
Mailer's multipart/attachment path, so per ADR 0028 `sent` is only `true` when the Mailer also
|
|
confirmed the attachment (`attachmentsProcessed == 1`); the result's `attachmentName` says
|
|
whether an attachment was part of the send. A PDF rendering failure is reported in the result
|
|
(`sent=false`, message names the error) and nothing is sent.
|
|
- Every probe emits the `fuchs.systemstatus.probes` counter tagged by component + status and an
|
|
`systemstatus.probe` activity span.
|
|
|
|
### Cards on the page
|
|
The page renders one non-probe system card, five refreshable connectivity cards (in this
|
|
order), and the non-refreshable startup-checks card. UI labels are from `fis.admin_txt_de.js`.
|
|
|
|
| Card (label) | Shows | Data source |
|
|
| --- | --- | --- |
|
|
| System | Hostname, OS, .NET framework, Environment, Test-Deployment flag, Prozess-ID, Laufzeit/Start (UTC) | `GetInfo()` — `Environment`/`Process.GetCurrentProcess()`, no network call |
|
|
| SQL-Datenbank (`database`) | Server, Datenbank, Login (never the password) | Config: `fuchs_fds_ConnectionString` parsed. Probe: `SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME()` |
|
|
| E-Mail-Dienst (`email`) | Mailer aktiv, Base-URL, Account, Server-ID, Token hinterlegt, OverrideRecipient/Umleitung aktiv | Config: `ProcessWebComSettings` / `FuchsEmailSettings`. Probe: `IComService.VerifyCredentialsAsync` → `GET {BaseUrl}/api/mailer/verify` at ProcessWeb, `ok` only when the Mailer API confirms account (+ server-id when configured) |
|
|
| Azure Blob Storage (`blob`) | Aktiviert, Konfiguriert, Rechnungs-/Mahnungs-Container, per-container file counts (Metrics) | Config: `AzureBlobStorageSettings` / `AzureBlobStorage_ConnectionString`. Probe: `IBlobStorageService.CheckConnectivityAsync` (account-info + blob count per container) |
|
|
| MFR (ERP) (`mfr`) | Host, Zugangsdaten hinterlegt, Sync aktiv | Config: `Fds:MFR_host` / `Fds:MFR_UserName` / `Fds:SyncEnabled`. Probe: `IMfrClientFactory.Create().GetEntities()` |
|
|
| Azure Key Vault (`keyvault`) | Vault-URI, App-Präfix, Verwaltete Secrets (count), Client registriert | Config: `SecretManagement:VaultUri`/`AppName`/`ManagedSecretKeys`, whether a `SecretClient` is DI-registered. Probe: reads the first key in `ManagedSecretKeys` via `SecretClient.GetSecretAsync` |
|
|
| Startup-Prüfungen (non-refreshable) | OK/Fehler/übersprungen per check: Key Vault, SQL-Datenbank, MFR, Spire.PDF-Lizenz, E-Mail (Startup-Probe) | `StartupCheckReporter.Latest`, captured once by `StartupSelfTestService` at boot; see next section |
|
|
|
|
Each connectivity card also carries a colored status pill (`ok`/`error`/`disabled`/`unconfigured`)
|
|
and a per-card "Aktualisieren" button (`POST /do/admin/probe/<component>`); the topbar "Alle
|
|
prüfen" button re-runs all five in parallel via `POST /do/admin/status`. The E-Mail-Dienst card
|
|
additionally hosts the test-email dialog (see `SendTestEmailAsync` below).
|
|
|
|
### Startup-checks widget (non-refreshable)
|
|
`StartupSelfTestService` runs the boot self-test once (Key Vault / Database / MFR / PDF-license /
|
|
mailer, gated by `Fuchs:StartupChecks:*`). It now also writes its outcome to the singleton
|
|
`StartupCheckReporter`, which `GetInfo()` returns as `StartupChecks`. The Admin page renders it as a
|
|
single **non-refreshable** card (there is no per-check retry — the live connectivity probes above
|
|
cover on-demand re-testing; the startup card is a historical record of the boot run). When the
|
|
self-test is disabled (`Enabled=false`, the appsettings default) or has not completed, the card shows
|
|
a "nicht ausgeführt" note. Each item shows OK / Fehler / übersprungen (a disabled check reports
|
|
`enabled=false`).
|
|
|
|
### Frontend (`fis.admin.js` + `fis.admin_txt_de.js` + `fis.admin.scss`)
|
|
Standard lazy-loaded module (same contract as `inv`/`rep`/`bam`). Renders a system card plus a
|
|
responsive grid of status cards; connectivity cards carry a colored status pill and a per-card
|
|
refresh button, the email card carries the test-email dialog. The topbar has an "Alle prüfen"
|
|
button that reloads the whole snapshot. Admin responses are serialized **camelCase** (the DTOs are
|
|
PascalCase) so the JS reads `probe.status`, `info.database.server`, etc.
|
|
|
|
## Key files
|
|
- `Fuchs/Controllers/IntranetController.Admin.cs` — `Do_Process_Admin` dispatch + shared auth gate/camelCase JSON helper (also used by the separate `usr` module, see [0015](../Decisions/0015-split-admin-user-management-module.md)).
|
|
- `Fuchs/Services/ISystemStatusService.cs`, `SystemStatusService.cs`, `SystemStatusModels.cs` — diagnostics service + DTOs.
|
|
- `Fuchs/Services/StartupCheckReport.cs` (`StartupCheckReporter` singleton) + `StartupSelfTestService.cs` — boot self-test result captured for the non-refreshable widget.
|
|
- `Fuchs/Services/IBlobStorageService.cs` / `AzureBlobStorageService.cs` — `CheckConnectivityAsync` + `BlobConnectivity`.
|
|
- `Fuchs/js/intranet/modules/fis.admin*.js`, `fis.admin.scss` — the module, texts, styles (bundled via `bdlconfig.json`).
|
|
- `Fuchs/js/intranet/fis_main_menu.js` (`addAdminMenuButtonsIfAuthorized`) + `fis_main_go.js` — conditional menu buttons (this module and `usr`).
|
|
- `Fuchs.Tests/SystemStatusServiceTests.cs` — service tests.
|
|
|
|
User account/group management moved to its own module and concept doc — see
|
|
[admin-user-management.md](admin-user-management.md).
|
|
|
|
## Related decisions
|
|
- [0011 — Admin module gated on `fds_sys` > 4](../Decisions/0011-admin-module-system-status.md)
|
|
- [0015 — Split Admin into separate Status/User-Management modules](../Decisions/0015-split-admin-user-management-module.md)
|