From 732aa5ef2bb03d0cb7e76e26d3d3b8b4511c0a10 Mon Sep 17 00:00:00 2001 From: LuRy Date: Mon, 4 May 2026 11:57:01 +0200 Subject: [PATCH] feat(postgresql): wire binary catalog integration --- src/daemon/NKS.WebDevConsole.Cli/Program.cs | 2 +- .../Binaries/CatalogClient.cs | 11 +++++++++++ src/daemon/NKS.WebDevConsole.Daemon/Program.cs | 14 ++++++++------ .../Services/WindowsFirewallManager.cs | 3 ++- src/frontend/src/components/pages/Settings.vue | 6 ++++++ .../src/components/shared/ServiceIcon.vue | 3 ++- src/frontend/src/locales/cs.json | 3 ++- src/frontend/src/locales/en.json | 3 ++- .../BinaryCatalogTests.cs | 1 + .../CatalogClientTests.cs | 15 ++++++++++++++- 10 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/daemon/NKS.WebDevConsole.Cli/Program.cs b/src/daemon/NKS.WebDevConsole.Cli/Program.cs index d6aabb0a..8cf85589 100644 --- a/src/daemon/NKS.WebDevConsole.Cli/Program.cs +++ b/src/daemon/NKS.WebDevConsole.Cli/Program.cs @@ -706,7 +706,7 @@ try { var installed = await client.GetJsonAsync("/api/binaries/installed"); - var keyApps = new[] { "apache", "php", "mysql", "node", "redis", "nginx", "mariadb" }; + var keyApps = new[] { "apache", "php", "mysql", "mariadb", "postgresql", "node", "redis", "nginx" }; foreach (var app in keyApps) { var first = installed.EnumerateArray() diff --git a/src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs b/src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs index e44d3b71..8fc5cd64 100644 --- a/src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs +++ b/src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs @@ -230,6 +230,17 @@ private static IEnumerable BuiltInFallback() ArchiveType: "tgz", Source: "github", UserAgent: null); + + yield return new BinaryRelease( + App: "postgresql", + Version: "18.3", + MajorMinor: "18", + Url: "https://get.enterprisedb.com/postgresql/postgresql-18.3-1-windows-x64-binaries.zip", + Os: "windows", + Arch: "x64", + ArchiveType: "zip", + Source: "enterprisedb", + UserAgent: null); } public IEnumerable ForApp(string app, string? os = null, string? arch = null) diff --git a/src/daemon/NKS.WebDevConsole.Daemon/Program.cs b/src/daemon/NKS.WebDevConsole.Daemon/Program.cs index 5332afb1..0826d473 100644 --- a/src/daemon/NKS.WebDevConsole.Daemon/Program.cs +++ b/src/daemon/NKS.WebDevConsole.Daemon/Program.cs @@ -662,11 +662,12 @@ static bool IsLoopbackPortFree(System.Net.IPAddress addr, int port) { "apache" or "caddy" or "nginx" => new[] { ("ports.http", "HttpPort"), ("ports.https", "HttpsPort") }, - "mysql" => new[] { ("ports.mysql", "Port") }, - "mariadb" => new[] { ("ports.mariadb", "Port") }, - "redis" => new[] { ("ports.redis", "Port") }, - "mailpit" => new[] { ("ports.mailpitSmtp", "SmtpPort"), - ("ports.mailpitHttp", "HttpPort") }, + "mysql" => new[] { ("ports.mysql", "Port") }, + "mariadb" => new[] { ("ports.mariadb", "Port") }, + "postgresql" => new[] { ("ports.postgresql", "Port") }, + "redis" => new[] { ("ports.redis", "Port") }, + "mailpit" => new[] { ("ports.mailpitSmtp", "SmtpPort"), + ("ports.mailpitHttp", "HttpPort") }, _ => Array.Empty<(string, string)>(), }; foreach (var (k, propName) in mapping) @@ -9624,7 +9625,7 @@ await conn.ExecuteAsync( { // Heuristic: match `ports.` or any port key that // looks like it could belong to this module (http/https map - // to apache/caddy/nginx; mysql/redis/mariadb keys map 1:1). + // to apache/caddy/nginx; database/cache keys map 1:1). var moduleId = module.ServiceId.ToLowerInvariant(); var relevant = portKeys.Any(k => { @@ -9668,6 +9669,7 @@ await conn.ExecuteAsync( "https" when moduleId == "apache" || moduleId == "caddy" || moduleId == "nginx" => "HttpsPort", "mysql" when moduleId == "mysql" => "Port", "mariadb" when moduleId == "mariadb" => "Port", + "postgresql" when moduleId == "postgresql" => "Port", "redis" when moduleId == "redis" => "Port", "mailpitsmtp" when moduleId == "mailpit" => "SmtpPort", "mailpithttp" when moduleId == "mailpit" => "HttpPort", diff --git a/src/daemon/NKS.WebDevConsole.Daemon/Services/WindowsFirewallManager.cs b/src/daemon/NKS.WebDevConsole.Daemon/Services/WindowsFirewallManager.cs index 6ff68187..f2a21f68 100644 --- a/src/daemon/NKS.WebDevConsole.Daemon/Services/WindowsFirewallManager.cs +++ b/src/daemon/NKS.WebDevConsole.Daemon/Services/WindowsFirewallManager.cs @@ -9,7 +9,7 @@ namespace NKS.WebDevConsole.Daemon.Services; /// /// Pre-registers Windows Defender Firewall inbound rules for NKS WDC managed /// service ports so the user doesn't get the per-first-bind UAC prompt each -/// time a managed binary (Apache, MySQL, Redis, Mailpit) opens a socket for +/// time a managed binary (Apache, MySQL, PostgreSQL, Redis, Mailpit) opens a socket for /// the first time. /// /// Rule naming convention: @@ -45,6 +45,7 @@ private static readonly (string Service, int Port)[] ManagedPorts = ("apache-http", 80), ("apache-https", 443), ("mysql", 3306), + ("postgresql", 5432), ("redis", 6379), ("mailpit-smtp", 1025), ("mailpit-web", 8025), diff --git a/src/frontend/src/components/pages/Settings.vue b/src/frontend/src/components/pages/Settings.vue index fecf2a84..f8d773cc 100644 --- a/src/frontend/src/components/pages/Settings.vue +++ b/src/frontend/src/components/pages/Settings.vue @@ -77,6 +77,9 @@ + + + @@ -1058,6 +1061,7 @@ const ports = reactive({ http: 80, https: 443, mysql: 3306, + postgresql: 5432, redis: 6379, mailpitSmtp: 1025, mailpitHttp: 8025, @@ -1687,6 +1691,7 @@ async function loadSettings() { deployUseLegacyHostHandlers.value = !(data['deploy.useLegacyHostHandlers'] === 'false' || data['deploy.useLegacyHostHandlers'] === '0') if (data['ports.mysql']) ports.mysql = parseInt(data['ports.mysql']) + if (data['ports.postgresql']) ports.postgresql = parseInt(data['ports.postgresql']) if (data['ports.redis']) ports.redis = parseInt(data['ports.redis']) if (data['ports.mailpitSmtp']) ports.mailpitSmtp = parseInt(data['ports.mailpitSmtp']) if (data['ports.mailpitHttp']) ports.mailpitHttp = parseInt(data['ports.mailpitHttp']) @@ -2559,6 +2564,7 @@ async function save() { 'ports.http': String(ports.http), 'ports.https': String(ports.https), 'ports.mysql': String(ports.mysql), + 'ports.postgresql': String(ports.postgresql), 'ports.redis': String(ports.redis), 'ports.mailpitSmtp': String(ports.mailpitSmtp), 'mcp.enabled': String(mcpEnabled.value), diff --git a/src/frontend/src/components/shared/ServiceIcon.vue b/src/frontend/src/components/shared/ServiceIcon.vue index 42bd79ac..c12e8d92 100644 --- a/src/frontend/src/components/shared/ServiceIcon.vue +++ b/src/frontend/src/components/shared/ServiceIcon.vue @@ -37,6 +37,7 @@ const SHORT_ID: Record = { 'nks.wdc.caddy': 'caddy', 'nks.wdc.php': 'php', 'nks.wdc.mysql': 'mysql', + 'nks.wdc.postgresql': 'postgresql', 'nks.wdc.redis': 'redis', 'nks.wdc.mailpit': 'mailpit', } @@ -58,7 +59,7 @@ const iconUrl = computed(() => { // headers, so pipe the token through the query string (the auth middleware // in Program.cs accepts `?token=` as a fallback to the Bearer header). // Without this every brand icon 401s and the sidebar fills up with "N". - const knownPlugin = ['apache', 'caddy', 'php', 'mysql', 'redis', 'mailpit'].includes(shortId) + const knownPlugin = ['apache', 'caddy', 'php', 'mysql', 'postgresql', 'redis', 'mailpit'].includes(shortId) if (knownPlugin) { const token = daemonToken() const suffix = token ? `?token=${encodeURIComponent(token)}` : '' diff --git a/src/frontend/src/locales/cs.json b/src/frontend/src/locales/cs.json index d840dc4d..44db1568 100644 --- a/src/frontend/src/locales/cs.json +++ b/src/frontend/src/locales/cs.json @@ -1716,6 +1716,7 @@ "httpPort": "HTTP port", "httpsPort": "HTTPS port", "mysqlPort": "MySQL port", + "postgresqlPort": "PostgreSQL port", "redisPort": "Redis port", "mailpitSmtp": "Mailpit SMTP", "mailpitHttp": "Mailpit HTTP", @@ -2093,4 +2094,4 @@ "sampleSiteWarn": "Ukázkový web možná nebyl vytvořen — zkontroluj stránku Weby", "dontShowAgain": "Příště nezobrazovat" } -} \ No newline at end of file +} diff --git a/src/frontend/src/locales/en.json b/src/frontend/src/locales/en.json index e7e72e4b..0d43af23 100644 --- a/src/frontend/src/locales/en.json +++ b/src/frontend/src/locales/en.json @@ -1714,6 +1714,7 @@ "httpPort": "HTTP Port", "httpsPort": "HTTPS Port", "mysqlPort": "MySQL Port", + "postgresqlPort": "PostgreSQL Port", "redisPort": "Redis Port", "mailpitSmtp": "Mailpit SMTP", "mailpitHttp": "Mailpit HTTP", @@ -2091,4 +2092,4 @@ "sampleSiteWarn": "Sample site may not have been created — check Sites page", "dontShowAgain": "Don't show again" } -} \ No newline at end of file +} diff --git a/tests/NKS.WebDevConsole.Core.Tests/BinaryCatalogTests.cs b/tests/NKS.WebDevConsole.Core.Tests/BinaryCatalogTests.cs index f6b985ee..d8a41f84 100644 --- a/tests/NKS.WebDevConsole.Core.Tests/BinaryCatalogTests.cs +++ b/tests/NKS.WebDevConsole.Core.Tests/BinaryCatalogTests.cs @@ -11,6 +11,7 @@ public void All_ContainsExpectedApps() Assert.Contains("apache", apps); Assert.Contains("php", apps); Assert.Contains("mysql", apps); + Assert.Contains("postgresql", apps); Assert.Contains("redis", apps); Assert.Contains("mailpit", apps); Assert.Contains("nginx", apps); diff --git a/tests/NKS.WebDevConsole.Daemon.Tests/CatalogClientTests.cs b/tests/NKS.WebDevConsole.Daemon.Tests/CatalogClientTests.cs index 433cbb8d..0b221884 100644 --- a/tests/NKS.WebDevConsole.Daemon.Tests/CatalogClientTests.cs +++ b/tests/NKS.WebDevConsole.Daemon.Tests/CatalogClientTests.cs @@ -63,7 +63,7 @@ public async Task RefreshAsync_ServerError_StillPopulatesBuiltInFallback() var count = await client.RefreshAsync(); - Assert.True(count >= 3); // at least the 3 cloudflared fallback entries + Assert.True(count >= 4); // cloudflared + PostgreSQL fallback entries Assert.NotEmpty(client.CachedReleases); Assert.NotEqual(DateTime.MinValue, client.LastFetch); } @@ -83,6 +83,19 @@ public async Task RefreshAsync_BuiltInFallback_IncludesCloudflaredForAllPlatform Assert.Single(macos); } + [Fact] + public async Task RefreshAsync_BuiltInFallback_IncludesPostgreSqlWindows() + { + var client = MakeClient(HttpStatusCode.InternalServerError); + await client.RefreshAsync(); + + var postgresql = client.ForApp("postgresql", os: "windows", arch: "x64").ToList(); + + Assert.Single(postgresql); + Assert.Equal("18.3", postgresql[0].Version); + Assert.Equal("zip", postgresql[0].ArchiveType); + } + [Fact] public async Task ForApp_FiltersByAppCaseInsensitive() {