diff --git a/Fuchs.Tests/DocumentMetadataBuilderTests.cs b/Fuchs.Tests/DocumentMetadataBuilderTests.cs index 7cd869b..357a24c 100644 --- a/Fuchs.Tests/DocumentMetadataBuilderTests.cs +++ b/Fuchs.Tests/DocumentMetadataBuilderTests.cs @@ -110,6 +110,20 @@ public class DocumentMetadataBuilderTests Assert.Equal(guid.ToString(), metadata["file_guid"]); } + [Theory] + [InlineData("Zahlungserinnerung für Müller", "Zahlungserinnerung f%C3%BCr M%C3%BCller")] + [InlineData("Rechnung\r\nKunde", "Rechnung%0D%0AKunde")] + public void Build_NonAsciiOrControlCharacters_PercentEncodesUtf8ForHttpHeaders( + string value, string expected) + { + var row = new Dictionary { ["DocumentName"] = value }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "DocumentName" }); + + Assert.Equal(expected, metadata["DocumentName"]); + Assert.All(metadata["DocumentName"], c => Assert.InRange(c, '\u0020', '\u007e')); + } + // ── Field-list edge cases ──────────────────────────────────────────────── [Theory] [InlineData("")] diff --git a/Fuchs.Tests/ProcessWebComServiceTests.cs b/Fuchs.Tests/ProcessWebComServiceTests.cs index fbfeb77..28d2248 100644 --- a/Fuchs.Tests/ProcessWebComServiceTests.cs +++ b/Fuchs.Tests/ProcessWebComServiceTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.Metrics; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -48,6 +49,12 @@ public class ProcessWebComServiceTests } } + private sealed class ThrowingHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => throw new HttpRequestException("simulated transport failure"); + } + private sealed class StubHttpClientFactory : IHttpClientFactory { private readonly HttpMessageHandler _handler; @@ -55,6 +62,41 @@ public class ProcessWebComServiceTests public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false); } + /// + /// Captures what would be written to fds__logEmail without hitting a database, by + /// overriding the audit-log write. Lets tests assert that every send path is logged and that + /// failures carry the service response / exception detail. + /// + private sealed class CapturingComService : ProcessWebComService + { + public readonly List<(bool success, List log)> Entries = new(); + + public CapturingComService( + IOptions settings, + IOptions emailSettings, + IHttpClientFactory httpClientFactory) + : base(NullLogger.Instance, intranet: null!, settings, emailSettings, httpClientFactory) + { + } + + protected override Task WriteAuditLogAsync(string reference, string guid, string config, + DateTime sent, bool success, IEnumerable errors) + { + Entries.Add((success, errors.ToList())); + return Task.CompletedTask; + } + } + + private static CapturingComService CreateCapturing(HttpMessageHandler handler, bool enabled = true, string? overrideRecipient = null) + { + var settings = Options.Create(new ProcessWebComSettings + { + Enabled = enabled, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" + }); + var emailSettings = Options.Create(new FuchsEmailSettings { OverrideRecipient = overrideRecipient }); + return new CapturingComService(settings, emailSettings, new StubHttpClientFactory(handler)); + } + private static ProcessWebComService CreateService(StubHandler handler, bool enabled = true, string? overrideRecipient = null) { var settings = Options.Create(new ProcessWebComSettings @@ -129,6 +171,102 @@ public class ProcessWebComServiceTests Assert.Equal(0, handler.CallCount); } + // ── Audit logging: every attempt is logged; failures carry the service response ─ + [Fact] + public async Task SendEmailAsync_Success_LogsSuccessfulAttempt() + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK)); + + await svc.SendEmailAsync("inv_log_ok", "S", "

x

", "kunde@example.de", "Kunde"); + + var entry = Assert.Single(svc.Entries); + Assert.True(entry.success); + } + + [Fact] + public async Task SendEmailAsync_ApiError_LogsFailureIncludingServiceResponse() + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.InternalServerError, "mailer rejected: quota exceeded")); + + bool result = await svc.SendEmailAsync("inv_log_api", "S", "

x

", "kunde@example.de", "Kunde"); + + Assert.False(result); + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.Contains("mailer rejected: quota exceeded")); // raw service response + Assert.Contains(entry.log, l => l.Contains("500")); // HTTP status + } + + [Fact] + public async Task SendEmailAsync_TransportException_LogsFailureIncludingExceptionDetail() + { + var svc = CreateCapturing(new ThrowingHandler()); + + bool result = await svc.SendEmailAsync("inv_log_ex", "S", "

x

", "kunde@example.de", "Kunde"); + + Assert.False(result); + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.Contains("Exception while sending")); + Assert.Contains(entry.log, l => l.Contains("simulated transport failure")); + } + + [Theory] + [InlineData("not-an-email")] + [InlineData("")] + public async Task SendEmailAsync_InvalidEmail_StillLogsFailedAttempt(string badEmail) + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK)); + + bool result = await svc.SendEmailAsync("inv_log_bad", "S", "

x

", badEmail, "Kunde"); + + Assert.False(result); + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.Contains("Invalid recipient email address")); + } + + [Fact] + public async Task SendEmailAsync_Disabled_LogsFailedAttempt() + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK), enabled: false); + + await svc.SendEmailAsync("inv_log_dis", "S", "

x

", "kunde@example.de", "Kunde"); + + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.ToLowerInvariant().Contains("disabled")); + } + + // ── Audit-log parameters honour the NOT NULL columns of fds__emaillog ────── + [Fact] + public void BuildAuditLogParameters_FailedAttempt_NeverPassesNullForNotNullColumns() + { + // fds__emaillog.config and .DateSent are NOT NULL; a failed/unset send must not send NULL + // (regression: it previously did, so every audit write failed and no email was ever logged). + var pl = ProcessWebComService.BuildAuditLogParameters( + "admin_test", guid: "", config: "", sent: default, success: false, errors: new[] { "err" }); + + object? config = pl.Single(p => p.ParameterName == "@config").Value; + object? dateSent = pl.Single(p => p.ParameterName == "@DateSent").Value; + + Assert.Equal("", config); + Assert.NotEqual(DBNull.Value, config); + Assert.IsType(dateSent); + Assert.NotEqual((object)DBNull.Value, dateSent); + } + + [Fact] + public void BuildAuditLogParameters_SuccessfulSend_KeepsProvidedSentTimestamp() + { + var when = new DateTime(2026, 7, 16, 12, 0, 0, DateTimeKind.Utc); + + var pl = ProcessWebComService.BuildAuditLogParameters( + "inv_1", guid: "g1", config: "", sent: when, success: true, errors: Array.Empty()); + + Assert.Equal(when, pl.Single(p => p.ParameterName == "@DateSent").Value); + } + // ── Dev/test recipient override safety net ───────────────────────────────── [Fact] public async Task SendEmailAsync_OverrideRecipientSet_RedirectsToOverrideAddress() diff --git a/Fuchs/Controllers/IntranetController.Admin.cs b/Fuchs/Controllers/IntranetController.Admin.cs index 29e24df..6fbea3e 100644 --- a/Fuchs/Controllers/IntranetController.Admin.cs +++ b/Fuchs/Controllers/IntranetController.Admin.cs @@ -85,8 +85,10 @@ public partial class IntranetController ContractResolver = new CamelCasePropertyNamesContractResolver(), }; + // Mirror OCORE's getJSONResult exactly (application/json; charset=utf-8) so the response + // parses identically to every other endpoint the frontend's postXT talks to. private ContentResult AdminJson(object payload) => - Content(JsonConvert.SerializeObject(payload, CamelCaseJson), "application/json"); + Content(JsonConvert.SerializeObject(payload, CamelCaseJson), "application/json; charset=utf-8"); /// /// Resolves the calling user's fds_sys module authorization level via the diff --git a/Fuchs/Fuchs.csproj b/Fuchs/Fuchs.csproj index 608e698..b99da14 100644 --- a/Fuchs/Fuchs.csproj +++ b/Fuchs/Fuchs.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/Fuchs/Services/DocumentMetadataBuilder.cs b/Fuchs/Services/DocumentMetadataBuilder.cs index 2c04a0e..8f4d09d 100644 --- a/Fuchs/Services/DocumentMetadataBuilder.cs +++ b/Fuchs/Services/DocumentMetadataBuilder.cs @@ -23,7 +23,7 @@ public static class DocumentMetadataBuilder { if (string.IsNullOrWhiteSpace(field)) continue; if (!TryGetValue(row, field, out object? value)) continue; - metadata[field] = Stringify(value); + metadata[field] = ToAsciiHeaderValue(Stringify(value)); } return metadata; } @@ -55,4 +55,24 @@ public static class DocumentMetadataBuilder DateTime dt => dt.ToString("O"), _ => value.ToString() ?? "" }; + + /// + /// Azure Blob metadata is transported in HTTP headers, whose values must contain ASCII + /// characters only. Preserve readable, printable ASCII and percent-encode every other + /// UTF-8 byte so titles and document names containing umlauts cannot break the upload. + /// + private static string ToAsciiHeaderValue(string value) + { + if (value.All(c => c is >= ' ' and <= '~')) return value; + + var result = new System.Text.StringBuilder(value.Length); + foreach (byte b in System.Text.Encoding.UTF8.GetBytes(value)) + { + if (b is >= 0x20 and <= 0x7E) + result.Append((char)b); + else + result.Append('%').Append(b.ToString("X2")); + } + return result.ToString(); + } } diff --git a/Fuchs/Services/ProcessWebComService.cs b/Fuchs/Services/ProcessWebComService.cs index 39a6e50..c7fea40 100644 --- a/Fuchs/Services/ProcessWebComService.cs +++ b/Fuchs/Services/ProcessWebComService.cs @@ -71,6 +71,9 @@ public class ProcessWebComService : IComService _logger.LogWarning("SendEmailAsync: invalid email address '{Email}' for ref {Reference}", email, reference); FuchsTelemetry.EmailsFailed.Add(1, new KeyValuePair("reason", "invalid-email")); act?.SetStatus(ActivityStatusCode.Error, "invalid-email"); + // Every attempt is audit-logged, including this early rejection (previously it returned unlogged). + await WriteAuditLogAsync(reference, "", "", default, false, + [$"Invalid recipient email address: '{email}'"]); return false; } @@ -116,7 +119,7 @@ public class ProcessWebComService : IComService _logger.LogDebug("SendEmailAsync ref={Reference} to={Email} attachments={Count}", reference, email, attachmentPayload.Length); - var (ok, responseBody) = await PostToApiAsync(reference, "e", communication); + var (ok, status, responseBody) = await PostToApiAsync(reference, "e", communication); if (ok) { success = true; @@ -124,13 +127,17 @@ public class ProcessWebComService : IComService } else { - errors.Add($"API error: {responseBody}"); - _logger.LogWarning("SendEmailAsync API error for {Reference}: {Body}", reference, responseBody); + // Persist the mailer's raw response so an unsuccessful send is diagnosable from the log. + errors.Add($"API error {(int)status} ({status}): {responseBody}"); + _logger.LogWarning("SendEmailAsync API error for {Reference}: {Status} {Body}", + reference, (int)status, responseBody); } } catch (Exception ex) { - errors.Add("Beim Versenden ist ein Fehler aufgetreten."); + // Capture the exception detail in the audit log (not just a generic message) so a + // transport/serialization failure is diagnosable after the fact. + errors.Add($"Exception while sending: {ex.GetType().Name}: {ex.Message}"); act?.SetStatus(ActivityStatusCode.Error, ex.Message); _logger.LogError(ex, "SendEmailAsync failed for {Reference}", reference); } @@ -171,7 +178,7 @@ public class ProcessWebComService : IComService body = message }; - var (ok, responseBody) = await PostToApiAsync(mobile, "s", communication); + var (ok, status, responseBody) = await PostToApiAsync(mobile, "s", communication); if (ok) { FuchsTelemetry.SmsSent.Add(1); @@ -179,7 +186,7 @@ public class ProcessWebComService : IComService return true; } - _logger.LogWarning("SendSmsAsync API error for {Mobile}: {Body}", mobile, responseBody); + _logger.LogWarning("SendSmsAsync API error for {Mobile}: {Status} {Body}", mobile, (int)status, responseBody); return false; } catch (Exception ex) @@ -191,7 +198,7 @@ public class ProcessWebComService : IComService // ── Private helpers ──────────────────────────────────────────────────────── - private async Task<(bool ok, string body)> PostToApiAsync(string reference, string comtype, object communication) + private async Task<(bool ok, System.Net.HttpStatusCode status, string body)> PostToApiAsync(string reference, string comtype, object communication) { var client = _httpClientFactory.CreateClient("ProcessWebMailer"); @@ -214,23 +221,21 @@ public class ProcessWebComService : IComService var response = await client.PostAsync($"{_settings.BaseUrl}/mailer/push_com", content); string responseBody = await response.Content.ReadAsStringAsync(); - return (response.IsSuccessStatusCode, responseBody); + return (response.IsSuccessStatusCode, response.StatusCode, responseBody); } - private async Task WriteAuditLogAsync(string reference, string guid, string config, + /// + /// Persists one communication attempt to [dbo].[fds__logEmail]. Called on every path — + /// success, API error (with the mailer's raw response in ), exceptions, + /// invalid address and the disabled-service case — so the email log is always complete. Wrapped + /// in try/catch so a logging failure never breaks (or masks) the send itself. Overridable for tests. + /// + protected virtual async Task WriteAuditLogAsync(string reference, string guid, string config, DateTime sent, bool success, IEnumerable errors) { try { - var pl = new List - { - SQL_VarChar("@Ref", reference), - SQL_VarChar("@guid", guid), - SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent), - SQL_NVarChar("@config", config, dbNull_IfEmpty: true), - SQL_Bit("@success", success), - SQL_NVarChar("@log", JsonConvert.SerializeObject(errors.ToList())) - }; + var pl = BuildAuditLogParameters(reference, guid, config, sent, success, errors); await setSQLValue_async( "EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;", _intranet.Intranet__SQLConnectionString, pl, @@ -242,6 +247,28 @@ public class ProcessWebComService : IComService } } + /// + /// Builds the parameter list for [dbo].[fds__logEmail]. fds__emaillog declares + /// config and DateSent as NOT NULL, so neither may be sent as NULL: + /// + /// @config is stored as an empty string (never NULL) when unset. + /// @DateSent falls back to the current (attempt) time when the send did not + /// complete — the success flag distinguishes a real send-time from a failed attempt. + /// + /// Pure/side-effect-free so the NOT NULL contract can be unit-tested without a database. + /// + internal static List BuildAuditLogParameters(string reference, string guid, + string config, DateTime sent, bool success, IEnumerable errors) => + new() + { + SQL_VarChar("@Ref", reference), + SQL_VarChar("@guid", guid ?? ""), + SQL_DateTime("@DateSent", sent == default ? DateTime.UtcNow : sent), + SQL_NVarChar("@config", config ?? ""), + SQL_Bit("@success", success), + SQL_NVarChar("@log", JsonConvert.SerializeObject(errors.ToList())) + }; + private string BuildSignature() { string sigPath = Path.Combine(AppContext.BaseDirectory, diff --git a/Fuchs/js/intranet/modules/fis.admin.js b/Fuchs/js/intranet/modules/fis.admin.js index 909a82a..5690598 100644 --- a/Fuchs/js/intranet/modules/fis.admin.js +++ b/Fuchs/js/intranet/modules/fis.admin.js @@ -230,18 +230,13 @@ let $adm = { button: $adt.testmail_btn, size: [420, 560], addcontent: $$.dc('adm_note').text($adt.tm_hint), - submit: function (e) { - let c = $(this).ldng(1); - let qs = c.serializeObject(true, { typedvalues: true }); - $ocms.postXT({ - url: $ocms.url('admin/testmail'), data: qs, timeout: 60000, success: function (r) { - let res = r.result || {}; - alert(res.message || (res.sent ? $adt.tm_sent : $adt.tm_failed)); - c.trigger('modal_close'); - }, error: function () { - alert($adt.tm_failed); - }, complete: function () { c.ldng(0); } - }); + url: $ocms.url('admin/testmail'), + typedvalues: true, + /* Runs inside jQuery's success dispatch — it must never throw, or postXT's + `complete` (which clears the page loading indicator) is skipped. */ + success: function (r) { + let res = (r && r.result) || {}; + alert(res.message || (res.sent === true ? $adt.tm_sent : $adt.tm_failed)); } }); } diff --git a/Fuchs/wwwroot/web/fis.admin.de.js b/Fuchs/wwwroot/web/fis.admin.de.js index 557a261..03d4c85 100644 --- a/Fuchs/wwwroot/web/fis.admin.de.js +++ b/Fuchs/wwwroot/web/fis.admin.de.js @@ -315,18 +315,13 @@ let $adm = { button: $adt.testmail_btn, size: [420, 560], addcontent: $$.dc('adm_note').text($adt.tm_hint), - submit: function (e) { - let c = $(this).ldng(1); - let qs = c.serializeObject(true, { typedvalues: true }); - $ocms.postXT({ - url: $ocms.url('admin/testmail'), data: qs, timeout: 60000, success: function (r) { - let res = r.result || {}; - alert(res.message || (res.sent ? $adt.tm_sent : $adt.tm_failed)); - c.trigger('modal_close'); - }, error: function () { - alert($adt.tm_failed); - }, complete: function () { c.ldng(0); } - }); + url: $ocms.url('admin/testmail'), + typedvalues: true, + /* Runs inside jQuery's success dispatch — it must never throw, or postXT's + `complete` (which clears the page loading indicator) is skipped. */ + success: function (r) { + let res = (r && r.result) || {}; + alert(res.message || (res.sent === true ? $adt.tm_sent : $adt.tm_failed)); } }); } diff --git a/Fuchs/wwwroot/web/fis.admin.de.min.js b/Fuchs/wwwroot/web/fis.admin.de.min.js index 31e259c..9219582 100644 --- a/Fuchs/wwwroot/web/fis.admin.de.min.js +++ b/Fuchs/wwwroot/web/fis.admin.de.min.js @@ -1 +1 @@ -let $adt={mdl:"Administration",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",tm_default_subject:"Fuchs Intranet – Test-E-Mail",tm_default_body:"Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.",loadfail:"Der Systemstatus konnte nicht geladen werden."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let n=$$.dc("adm_grid",e);$adm.dbCard(n,t.database||{},d.database),$adm.mailCard(n,t.email||{}),$adm.blobCard(n,t.blob||{},d.blob),$adm.mfrCard(n,t.mfr||{},d.mfr),$adm.kvCard(n,t.keyVault||{},d.keyvault)},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let n=$$.td(d).addClass("v");t[1]instanceof jQuery?n.append(t[1]):n.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,n){let r=$$.dc("adm_card",e),i=$$.dc("adm_card_hd",r);$$.s(t).appendTo(i);let s=$$.sc("adm_pill").appendTo(i),o=$$.bbtn($adt.refresh,"adm_btn").appendTo(i),c=$$.dc("adm_card_bd",r);$adm.kvTable(c,d);let l=$$.dc("adm_probe",c),m=$$.dc("adm_probe_msg",l),u=$$.dc("adm_probe_metrics",l),_=$$.dc("adm_probe_det",l),p=$$.dc("adm_probe_dur",l),b=function(e){e=e||{},$adm.setPill(s,e),m.text(e.message||""),u.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",u);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),_.text(e.detail||""),p.text(null!=e.durationMs?e.durationMs+" ms":"")};return b(n),o.click((function(){$adm.setPill(s,{status:"checking"}),m.text(""),u.empty(),_.text(""),p.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){b(e.probe)},error:function(){$adm.setPill(s,{status:"error"}),m.text($adt.st_error)}})})),r},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let n=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&n.aC("adm_env_test"),n.appendTo(d);let r=$$.dc("adm_card_bd",a);$adm.kvTable(r,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let n=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",n).text($adt.startup_notrun);let r=t.items||[],i=!1;$.each(r,(function(e,t){!0===t.enabled&&!0!==t.ok&&(i=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:i?"error":"ok"}),$adm.kvTable(n,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let s=$$.dc("adm_checks",n);$.each(r,(function(e,t){let a=$$.dc("adm_check",s);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",n={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(n).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t){let a=$$.dc("adm_card",e),d=$$.dc("adm_card_hd",a);$$.s($adt.mail).appendTo(d),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:!0===t.mailerEnabled?"ok":"disabled"});let n=$$.dc("adm_card_bd",a),r=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&r.push([$adt.override,t.overrideRecipient]),$adm.kvTable(n,r);let i=$$.dc("adm_card_ft",a);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(i).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),submit:function(e){let t=$(this).ldng(1),a=t.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("admin/testmail"),data:a,timeout:6e4,success:function(e){let a=e.result||{};alert(a.message||(a.sent?$adt.tm_sent:$adt.tm_failed)),t.trigger("modal_close")},error:function(){alert($adt.tm_failed)},complete:function(){t.ldng(0)}})}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file +let $adt={mdl:"Administration",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",tm_default_subject:"Fuchs Intranet – Test-E-Mail",tm_default_body:"Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.",loadfail:"Der Systemstatus konnte nicht geladen werden."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let r=$$.dc("adm_grid",e);$adm.dbCard(r,t.database||{},d.database),$adm.mailCard(r,t.email||{}),$adm.blobCard(r,t.blob||{},d.blob),$adm.mfrCard(r,t.mfr||{},d.mfr),$adm.kvCard(r,t.keyVault||{},d.keyvault)},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let r=$$.td(d).addClass("v");t[1]instanceof jQuery?r.append(t[1]):r.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,r){let n=$$.dc("adm_card",e),i=$$.dc("adm_card_hd",n);$$.s(t).appendTo(i);let s=$$.sc("adm_pill").appendTo(i),o=$$.bbtn($adt.refresh,"adm_btn").appendTo(i),c=$$.dc("adm_card_bd",n);$adm.kvTable(c,d);let l=$$.dc("adm_probe",c),m=$$.dc("adm_probe_msg",l),u=$$.dc("adm_probe_metrics",l),_=$$.dc("adm_probe_det",l),p=$$.dc("adm_probe_dur",l),b=function(e){e=e||{},$adm.setPill(s,e),m.text(e.message||""),u.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",u);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),_.text(e.detail||""),p.text(null!=e.durationMs?e.durationMs+" ms":"")};return b(r),o.click((function(){$adm.setPill(s,{status:"checking"}),m.text(""),u.empty(),_.text(""),p.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){b(e.probe)},error:function(){$adm.setPill(s,{status:"error"}),m.text($adt.st_error)}})})),n},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let r=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&r.aC("adm_env_test"),r.appendTo(d);let n=$$.dc("adm_card_bd",a);$adm.kvTable(n,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let r=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",r).text($adt.startup_notrun);let n=t.items||[],i=!1;$.each(n,(function(e,t){!0===t.enabled&&!0!==t.ok&&(i=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:i?"error":"ok"}),$adm.kvTable(r,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let s=$$.dc("adm_checks",r);$.each(n,(function(e,t){let a=$$.dc("adm_check",s);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",r={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(r).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t){let a=$$.dc("adm_card",e),d=$$.dc("adm_card_hd",a);$$.s($adt.mail).appendTo(d),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:!0===t.mailerEnabled?"ok":"disabled"});let r=$$.dc("adm_card_bd",a),n=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&n.push([$adt.override,t.overrideRecipient]),$adm.kvTable(r,n);let i=$$.dc("adm_card_ft",a);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(i).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),url:$ocms.url("admin/testmail"),typedvalues:!0,success:function(e){let t=e&&e.result||{};alert(t.message||(!0===t.sent?$adt.tm_sent:$adt.tm_failed))}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file