Compare commits

...

6 Commits

Author SHA1 Message Date
60d095cd78 v13.9.2
Some checks failed
Docker (tags) / security (push) Failing after 2m58s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-12 19:42:07 +00:00
2861511d20 fix(web-ui): improve form field descriptions and align certificate settings with tile components 2026-04-12 19:42:07 +00:00
b582d44502 v13.9.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-08 15:26:12 +00:00
36a2ebc94e fix(network-ui): enable flashing table updates for network activity, remote ingress, and VPN views 2026-04-08 15:26:12 +00:00
ed52a3188d v13.9.0
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-08 14:54:49 +00:00
93cc5c7b06 feat(dns): add built-in dcrouter DNS provider support and rename manual domains to dcrouter-hosted/local 2026-04-08 14:54:49 +00:00
28 changed files with 415 additions and 256 deletions

View File

@@ -1,5 +1,28 @@
# Changelog # Changelog
## 2026-04-12 - 13.9.2 - fix(web-ui)
improve form field descriptions and align certificate settings with tile components
- Refines labels and adds descriptive helper text across API token, DNS, domain, route, edge, target profile, and VPN forms for clearer operator input
- Updates the DNS provider form to surface provider and credential guidance through built-in input metadata instead of custom help blocks
- Restyles the certificates ACME settings section to use tile-based layout and improves related form wording and file upload metadata
- Refreshes the Cloudflare DNS provider description and bumps UI-related dependencies
## 2026-04-08 - 13.9.1 - fix(network-ui)
enable flashing table updates for network activity, remote ingress, and VPN views
- adds stable row keys to dees-table instances so existing rows can be diffed correctly
- enables flash highlighting for changed rows and cells across network activity, top IPs, backends, remote ingress edges, and VPN clients
- updates network activity request data on every refresh so live metrics like duration and byte counts visibly refresh
## 2026-04-08 - 13.9.0 - feat(dns)
add built-in dcrouter DNS provider support and rename manual domains to dcrouter-hosted/local
- Expose a synthetic built-in "DcRouter" provider in provider listings and block create, edit, delete, test, and external domain listing operations for it
- Rename domain and record source semantics from "manual" to "dcrouter" and "local" across backend, interfaces, and UI
- Add database migrations to convert existing DomainDoc.source and DnsRecordDoc.source values to the new naming
- Update domain creation flows and provider UI labels to reflect dcrouter-hosted authoritative domains
## 2026-04-08 - 13.8.0 - feat(acme) ## 2026-04-08 - 13.8.0 - feat(acme)
add DB-backed ACME configuration management and OpsServer certificate settings UI add DB-backed ACME configuration management and OpsServer certificate settings UI

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "13.8.0", "version": "13.9.2",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -27,7 +27,7 @@
"@git.zone/tsrun": "^2.0.2", "@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.3", "@git.zone/tstest": "^3.6.3",
"@git.zone/tswatch": "^3.3.2", "@git.zone/tswatch": "^3.3.2",
"@types/node": "^25.5.2" "@types/node": "^25.6.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.3.0", "@api.global/typedrequest": "^3.3.0",
@@ -35,7 +35,7 @@
"@api.global/typedserver": "^8.4.6", "@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2", "@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.69.1", "@design.estate/dees-catalog": "^3.76.1",
"@design.estate/dees-element": "^2.2.4", "@design.estate/dees-element": "^2.2.4",
"@push.rocks/lik": "^6.4.0", "@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.1.0", "@push.rocks/projectinfo": "^5.1.0",
@@ -49,7 +49,7 @@
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.2.2", "@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartmetrics": "^3.0.3", "@push.rocks/smartmetrics": "^3.0.3",
"@push.rocks/smartmigration": "1.1.1", "@push.rocks/smartmigration": "1.2.0",
"@push.rocks/smartmta": "^5.3.1", "@push.rocks/smartmta": "^5.3.1",
"@push.rocks/smartnetwork": "^4.5.2", "@push.rocks/smartnetwork": "^4.5.2",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
@@ -67,7 +67,7 @@
"@serve.zone/remoteingress": "^4.15.3", "@serve.zone/remoteingress": "^4.15.3",
"@tsclass/tsclass": "^9.5.0", "@tsclass/tsclass": "^9.5.0",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"lru-cache": "^11.3.2", "lru-cache": "^11.3.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"uuid": "^13.0.0" "uuid": "^13.0.0"
}, },

113
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^3.69.1 specifier: ^3.76.1
version: 3.69.1(@tiptap/pm@2.27.2) version: 3.76.1(@tiptap/pm@2.27.2)
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.2.4 specifier: ^2.2.4
version: 2.2.4 version: 2.2.4
@@ -66,8 +66,8 @@ importers:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
'@push.rocks/smartmigration': '@push.rocks/smartmigration':
specifier: 1.1.1 specifier: 1.2.0
version: 1.1.1(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7)) version: 1.2.0(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))
'@push.rocks/smartmta': '@push.rocks/smartmta':
specifier: ^5.3.1 specifier: ^5.3.1
version: 5.3.1 version: 5.3.1
@@ -120,8 +120,8 @@ importers:
specifier: ^1.5.6 specifier: ^1.5.6
version: 1.5.6 version: 1.5.6
lru-cache: lru-cache:
specifier: ^11.3.2 specifier: ^11.3.3
version: 11.3.2 version: 11.3.3
qrcode: qrcode:
specifier: ^1.5.4 specifier: ^1.5.4
version: 1.5.4 version: 1.5.4
@@ -145,8 +145,8 @@ importers:
specifier: ^3.3.2 specifier: ^3.3.2
version: 3.3.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tiptap/pm@2.27.2) version: 3.3.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tiptap/pm@2.27.2)
'@types/node': '@types/node':
specifier: ^25.5.2 specifier: ^25.6.0
version: 25.5.2 version: 25.6.0
packages: packages:
@@ -353,8 +353,8 @@ packages:
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.69.1': '@design.estate/dees-catalog@3.76.1':
resolution: {integrity: sha512-OSpHB/hfOrL2mkAfF50TqTKJ2hvPd7Cj1WklAmFckyjloE4fd7DRDeXdI/Bziq9152gExipX5VoofTAOr4rF5w==} resolution: {integrity: sha512-DSnu1NHz0C9CI13e6HMUV6lFiAKzOoPccZUZu6wDrpTcGha1trvFftcRzsieJ0NrvNJ6qZrh1vGL6ZYhu5RO0A==}
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -368,6 +368,9 @@ packages:
'@design.estate/dees-wcctools@3.8.0': '@design.estate/dees-wcctools@3.8.0':
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==} resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
'@design.estate/dees-wcctools@3.8.4':
resolution: {integrity: sha512-KpFK/azK+a/Xpq33pXKcho+tdFKVHhKZM5ArvHqo9QMwTczgp5DZZgowTDUuqAofjZwnuVfCPHK/Pw9e64N46A==}
'@emnapi/core@1.9.2': '@emnapi/core@1.9.2':
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
@@ -1231,8 +1234,8 @@ packages:
'@push.rocks/smartmetrics@3.0.3': '@push.rocks/smartmetrics@3.0.3':
resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==} resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==}
'@push.rocks/smartmigration@1.1.1': '@push.rocks/smartmigration@1.2.0':
resolution: {integrity: sha512-K/eLN9cNy+CLOT73rhI93vOy/vGwpV46iJpjRUyPwHsMcQcV6po2idk5+XZQzeuq2x7KpKuUPtZ6gXMtf5Y/ig==} resolution: {integrity: sha512-H2diE1UbZm4cXjxgxkt2YQW3aUQ3QVVU/e8Ws30hzIep0xIqL1BH6//WawA5ZBQhnAOBssZpVOuWOd3GIeBq+Q==}
peerDependencies: peerDependencies:
'@push.rocks/smartbucket': ^4.6.0 '@push.rocks/smartbucket': ^4.6.0
'@push.rocks/smartdata': ^7.1.7 '@push.rocks/smartdata': ^7.1.7
@@ -2054,8 +2057,8 @@ packages:
'@types/node@22.19.17': '@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@types/node@25.5.2': '@types/node@25.6.0':
resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
'@types/qrcode@1.5.6': '@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
@@ -2858,8 +2861,8 @@ packages:
humanize-ms@1.2.1: humanize-ms@1.2.1:
resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=}
ibantools@4.5.2: ibantools@4.5.4:
resolution: {integrity: sha512-is+8TgZcKS/AMv/z9nW1zz0bhjhoyjpA1p0nc3A6GkW/InOdcQiUZpkufADzh/aO/LY/TOD/P3oPWncNRn5QMA==} resolution: {integrity: sha512-6jX1gh4aH6XH+o0ey+wtkMTzkcvsEta7DakIOZSng9voZYpMw3U+gK1+tZChk3aRcPcloEt0NOzksjaRZiqXbw==}
iconv-lite@0.4.24: iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
@@ -3076,16 +3079,16 @@ packages:
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
lru-cache@11.3.2: lru-cache@11.3.3:
resolution: {integrity: sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==} resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
lru-cache@7.18.3: lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'} engines: {node: '>=12'}
lucide@0.577.0: lucide@1.8.0:
resolution: {integrity: sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==} resolution: {integrity: sha512-JjV/QnadgFLj1Pyu9IKl0lknrolFEzo04B64QcYLLeRzZl/iEHpdbSrRRKbyXcv45SZNv+WGjIUCT33e7xHO6Q==}
mailparser@3.9.6: mailparser@3.9.6:
resolution: {integrity: sha512-EJYTDWMrOS1kddK1mTsRkrx2Ngh2nYsg54SRMWVVWGVEGbHH4tod8tqqU9hIRPgGQVboSjFubDn9cboSitbM3Q==} resolution: {integrity: sha512-EJYTDWMrOS1kddK1mTsRkrx2Ngh2nYsg54SRMWVVWGVEGbHH4tod8tqqU9hIRPgGQVboSjFubDn9cboSitbM3Q==}
@@ -4098,8 +4101,8 @@ packages:
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.18.2: undici-types@7.19.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
unified@11.0.5: unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -4315,7 +4318,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3) '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
'@cloudflare/workers-types': 4.20260405.1 '@cloudflare/workers-types': 4.20260405.1
'@design.estate/dees-catalog': 3.69.1(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.76.1(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.4.0 '@push.rocks/lik': 6.4.0
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -4844,11 +4847,11 @@ snapshots:
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.69.1(@tiptap/pm@2.27.2)': '@design.estate/dees-catalog@3.76.1(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4 '@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.0 '@design.estate/dees-wcctools': 3.8.4
'@fortawesome/fontawesome-svg-core': 7.2.0 '@fortawesome/fontawesome-svg-core': 7.2.0
'@fortawesome/free-brands-svg-icons': 7.2.0 '@fortawesome/free-brands-svg-icons': 7.2.0
'@fortawesome/free-regular-svg-icons': 7.2.0 '@fortawesome/free-regular-svg-icons': 7.2.0
@@ -4866,9 +4869,9 @@ snapshots:
'@tsclass/tsclass': 9.5.0 '@tsclass/tsclass': 9.5.0
echarts: 5.6.0 echarts: 5.6.0
highlight.js: 11.11.1 highlight.js: 11.11.1
ibantools: 4.5.2 ibantools: 4.5.4
lightweight-charts: 5.1.0 lightweight-charts: 5.1.0
lucide: 0.577.0 lucide: 1.8.0
monaco-editor: 0.55.1 monaco-editor: 0.55.1
pdfjs-dist: 4.10.38 pdfjs-dist: 4.10.38
xterm: 5.3.0 xterm: 5.3.0
@@ -4937,6 +4940,18 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@design.estate/dees-wcctools@3.8.4':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.2
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@emnapi/core@1.9.2': '@emnapi/core@1.9.2':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.2.1 '@emnapi/wasi-threads': 1.2.1
@@ -6354,7 +6369,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.2.2 '@push.rocks/smartlog': 3.2.2
'@push.rocks/smartmigration@1.1.1(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))': '@push.rocks/smartmigration@1.2.0(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))':
dependencies: dependencies:
'@push.rocks/smartlog': 3.2.2 '@push.rocks/smartlog': 3.2.2
'@push.rocks/smartversion': 3.1.0 '@push.rocks/smartversion': 3.1.0
@@ -6404,7 +6419,7 @@ snapshots:
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrust': 1.3.2 '@push.rocks/smartrust': 1.3.2
'@tsclass/tsclass': 9.5.0 '@tsclass/tsclass': 9.5.0
lru-cache: 11.3.2 lru-cache: 11.3.3
mailparser: 3.9.6 mailparser: 3.9.6
uuid: 13.0.0 uuid: 13.0.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -6900,7 +6915,7 @@ snapshots:
'@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)': '@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-catalog': 3.69.1(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.76.1(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4 '@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.0 '@design.estate/dees-wcctools': 3.8.0
@@ -7442,7 +7457,7 @@ snapshots:
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
source-map: 0.6.1 source-map: 0.6.1
'@types/debug@4.1.13': '@types/debug@4.1.13':
@@ -7452,7 +7467,7 @@ snapshots:
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -7472,12 +7487,12 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
@@ -7498,16 +7513,16 @@ snapshots:
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/node-fetch@2.6.13': '@types/node-fetch@2.6.13':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
form-data: 4.0.5 form-data: 4.0.5
'@types/node-forge@1.3.14': '@types/node-forge@1.3.14':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/node@16.9.1': {} '@types/node@16.9.1': {}
@@ -7519,13 +7534,13 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.5.2': '@types/node@25.6.0':
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.19.2
'@types/qrcode@1.5.6': '@types/qrcode@1.5.6':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/randomatic@3.1.5': {} '@types/randomatic@3.1.5': {}
@@ -7535,11 +7550,11 @@ snapshots:
'@types/tar-stream@3.1.4': '@types/tar-stream@3.1.4':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -7569,11 +7584,11 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 25.5.2 '@types/node': 25.6.0
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -8390,7 +8405,7 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
ibantools@4.5.2: {} ibantools@4.5.4: {}
iconv-lite@0.4.24: iconv-lite@0.4.24:
dependencies: dependencies:
@@ -8628,11 +8643,11 @@ snapshots:
lowercase-keys@3.0.0: {} lowercase-keys@3.0.0: {}
lru-cache@11.3.2: {} lru-cache@11.3.3: {}
lru-cache@7.18.3: {} lru-cache@7.18.3: {}
lucide@0.577.0: {} lucide@1.8.0: {}
mailparser@3.9.6: mailparser@3.9.6:
dependencies: dependencies:
@@ -9268,7 +9283,7 @@ snapshots:
path-scurry@2.0.2: path-scurry@2.0.2:
dependencies: dependencies:
lru-cache: 11.3.2 lru-cache: 11.3.3
minipass: 7.1.3 minipass: 7.1.3
path-to-regexp@8.4.2: {} path-to-regexp@8.4.2: {}
@@ -9942,7 +9957,7 @@ snapshots:
undici-types@6.21.0: {} undici-types@6.21.0: {}
undici-types@7.18.2: {} undici-types@7.19.2: {}
unified@11.0.5: unified@11.0.5:
dependencies: dependencies:

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '13.8.0', version: '13.9.2',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -1792,7 +1792,8 @@ export class DcRouter {
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`); logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`);
} }
// Hand the DnsServer to DnsManager so DB-backed manual records get registered too. // Hand the DnsServer to DnsManager so DB-backed local records on
// dcrouter-hosted domains get registered too.
if (this.dnsManager && this.dnsServer) { if (this.dnsManager && this.dnsServer) {
await this.dnsManager.attachDnsServer(this.dnsServer); await this.dnsManager.attachDnsServer(this.dnsServer);
} }

View File

@@ -25,9 +25,9 @@ import type {
* Responsibilities: * Responsibilities:
* - Load Domain/DnsRecord docs from the DB on start * - Load Domain/DnsRecord docs from the DB on start
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains) * - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
* - Register manual-domain records with smartdns.DnsServer at startup * - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
* - Provide CRUD methods used by OpsServer handlers (manual domains hit smartdns, * - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
* provider domains hit the provider API) * smartdns, provider domains hit the provider API)
* - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy() * - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy()
* *
* Provider-managed domains are NEVER served from the embedded DnsServer — the * Provider-managed domains are NEVER served from the embedded DnsServer — the
@@ -69,12 +69,12 @@ export class DnsManager {
/** /**
* Wire the embedded DnsServer instance after it has been created by * Wire the embedded DnsServer instance after it has been created by
* DcRouter.setupDnsWithSocketHandler(). After this, manual records loaded * DcRouter.setupDnsWithSocketHandler(). After this, local records on
* from the DB are registered with the server. * dcrouter-hosted domains loaded from the DB are registered with the server.
*/ */
public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> { public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> {
this.dnsServer = dnsServer; this.dnsServer = dnsServer;
await this.applyManualDomainsToDnsServer(); await this.applyDcrouterDomainsToDnsServer();
} }
// ========================================================================== // ==========================================================================
@@ -83,7 +83,8 @@ export class DnsManager {
/** /**
* If no DomainDocs exist yet but the constructor has legacy DNS fields, * If no DomainDocs exist yet but the constructor has legacy DNS fields,
* seed them as `source: 'manual'` records. On subsequent boots (DB has * seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
* local (`record.source: 'local'`) records. On subsequent boots (DB has
* entries), constructor config is ignored with a warning. * entries), constructor config is ignored with a warning.
*/ */
private async seedFromConstructorConfigIfEmpty(): Promise<void> { private async seedFromConstructorConfigIfEmpty(): Promise<void> {
@@ -117,7 +118,7 @@ export class DnsManager {
const domain = new DomainDoc(); const domain = new DomainDoc();
domain.id = plugins.uuid.v4(); domain.id = plugins.uuid.v4();
domain.name = scope.toLowerCase(); domain.name = scope.toLowerCase();
domain.source = 'manual'; domain.source = 'dcrouter';
domain.authoritative = true; domain.authoritative = true;
domain.createdAt = now; domain.createdAt = now;
domain.updatedAt = now; domain.updatedAt = now;
@@ -144,7 +145,7 @@ export class DnsManager {
record.type = rec.type as TDnsRecordType; record.type = rec.type as TDnsRecordType;
record.value = rec.value; record.value = rec.value;
record.ttl = rec.ttl ?? 300; record.ttl = rec.ttl ?? 300;
record.source = 'manual'; record.source = 'local';
record.createdAt = now; record.createdAt = now;
record.updatedAt = now; record.updatedAt = now;
record.createdBy = 'seed'; record.createdBy = 'seed';
@@ -174,28 +175,31 @@ export class DnsManager {
} }
// ========================================================================== // ==========================================================================
// Manual-domain DnsServer wiring // DcRouter-hosted domain DnsServer wiring
// ========================================================================== // ==========================================================================
/** /**
* Register all manual-domain records from the DB with the embedded DnsServer. * Register all records from dcrouter-hosted domains in the DB with the
* Called once after attachDnsServer(). * embedded DnsServer. Called once after attachDnsServer().
*/ */
private async applyManualDomainsToDnsServer(): Promise<void> { private async applyDcrouterDomainsToDnsServer(): Promise<void> {
if (!this.dnsServer) { if (!this.dnsServer) {
return; return;
} }
const allDomains = await DomainDoc.findAll(); const allDomains = await DomainDoc.findAll();
const manualDomains = allDomains.filter((d) => d.source === 'manual'); const dcrouterDomains = allDomains.filter((d) => d.source === 'dcrouter');
let registered = 0; let registered = 0;
for (const domain of manualDomains) { for (const domain of dcrouterDomains) {
const records = await DnsRecordDoc.findByDomainId(domain.id); const records = await DnsRecordDoc.findByDomainId(domain.id);
for (const rec of records) { for (const rec of records) {
this.registerRecordWithDnsServer(rec); this.registerRecordWithDnsServer(rec);
registered++; registered++;
} }
} }
logger.log('info', `DnsManager: registered ${registered} manual DNS record(s) from DB`); logger.log(
'info',
`DnsManager: registered ${registered} dcrouter-hosted DNS record(s) from DB`,
);
} }
/** /**
@@ -381,6 +385,12 @@ export class DnsManager {
credentials: TDnsProviderCredentials; credentials: TDnsProviderCredentials;
createdBy: string; createdBy: string;
}): Promise<string> { }): Promise<string> {
if (args.type === 'dcrouter') {
throw new Error(
'createProvider: cannot create a DnsProviderDoc with type "dcrouter" — ' +
'that type is reserved for the built-in pseudo-provider surfaced at read time.',
);
}
const now = Date.now(); const now = Date.now();
const doc = new DnsProviderDoc(); const doc = new DnsProviderDoc();
doc.id = plugins.uuid.v4(); doc.id = plugins.uuid.v4();
@@ -473,10 +483,10 @@ export class DnsManager {
} }
/** /**
* Create a manual (authoritative) domain. dcrouter will serve DNS records * Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
* for this domain via the embedded smartdns.DnsServer. * DNS records for this domain via the embedded smartdns.DnsServer.
*/ */
public async createManualDomain(args: { public async createDcrouterDomain(args: {
name: string; name: string;
description?: string; description?: string;
createdBy: string; createdBy: string;
@@ -485,7 +495,7 @@ export class DnsManager {
const doc = new DomainDoc(); const doc = new DomainDoc();
doc.id = plugins.uuid.v4(); doc.id = plugins.uuid.v4();
doc.name = args.name.toLowerCase(); doc.name = args.name.toLowerCase();
doc.source = 'manual'; doc.source = 'dcrouter';
doc.authoritative = true; doc.authoritative = true;
doc.description = args.description; doc.description = args.description;
doc.createdAt = now; doc.createdAt = now;
@@ -571,10 +581,11 @@ export class DnsManager {
/** /**
* Delete a domain and all of its DNS records. For provider domains, only * Delete a domain and all of its DNS records. For provider domains, only
* removes the local mirror — does NOT touch the provider. * removes the local mirror — does NOT touch the provider.
* For manual domains, also unregisters records from the embedded DnsServer. * For dcrouter-hosted domains, also unregisters records from the embedded
* DnsServer.
* *
* Note: smartdns has no public unregister-by-name API in the version pinned * Note: smartdns has no public unregister-by-name API in the version pinned
* here, so manual record deletes only take effect after a restart. The DB * here, so local record deletes only take effect after a restart. The DB
* is the source of truth and the next start will not register the deleted * is the source of truth and the next start will not register the deleted
* record. * record.
*/ */
@@ -652,7 +663,7 @@ export class DnsManager {
doc.value = args.value; doc.value = args.value;
doc.ttl = args.ttl ?? 300; doc.ttl = args.ttl ?? 300;
if (args.proxied !== undefined) doc.proxied = args.proxied; if (args.proxied !== undefined) doc.proxied = args.proxied;
doc.source = 'manual'; doc.source = 'local';
doc.createdAt = now; doc.createdAt = now;
doc.updatedAt = now; doc.updatedAt = now;
doc.createdBy = args.createdBy; doc.createdBy = args.createdBy;
@@ -678,7 +689,7 @@ export class DnsManager {
return { success: false, message: `Provider rejected record: ${(err as Error).message}` }; return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
} }
} else { } else {
// Manual / authoritative — register with embedded DnsServer immediately // dcrouter-hosted / authoritative — register with embedded DnsServer immediately
this.registerRecordWithDnsServer(doc); this.registerRecordWithDnsServer(doc);
} }
@@ -722,7 +733,7 @@ export class DnsManager {
return { success: false, message: `Provider rejected update: ${(err as Error).message}` }; return { success: false, message: `Provider rejected update: ${(err as Error).message}` };
} }
} else { } else {
// Re-register the manual record so the new closure picks up the updated fields // Re-register the local record so the new closure picks up the updated fields
this.registerRecordWithDnsServer(doc); this.registerRecordWithDnsServer(doc);
} }
@@ -748,7 +759,7 @@ export class DnsManager {
} }
} }
} }
// For manual records: smartdns has no unregister API in the pinned version, // For local records: smartdns has no unregister API in the pinned version,
// so the record stays served until the next restart. The DB delete still // so the record stays served until the next restart. The DB delete still
// takes effect — on restart, the record will not be re-registered. // takes effect — on restart, the record will not be re-registered.
@@ -807,7 +818,7 @@ export class DnsManager {
public toPublicDomain(doc: DomainDoc): { public toPublicDomain(doc: DomainDoc): {
id: string; id: string;
name: string; name: string;
source: 'manual' | 'provider'; source: 'dcrouter' | 'provider';
providerId?: string; providerId?: string;
authoritative: boolean; authoritative: boolean;
nameservers?: string[]; nameservers?: string[];

View File

@@ -38,6 +38,17 @@ export function createDnsProvider(
} }
return new CloudflareDnsProvider(credentials.apiToken); return new CloudflareDnsProvider(credentials.apiToken);
} }
case 'dcrouter': {
// The built-in DcRouter pseudo-provider has no runtime client — dcrouter
// itself serves the records via the embedded smartdns.DnsServer. This
// case exists only to satisfy the exhaustive switch; it should never
// actually run because the handler layer rejects any CRUD that would
// result in a DnsProviderDoc with type: 'dcrouter'.
throw new Error(
`createDnsProvider: 'dcrouter' is a built-in pseudo-provider — no runtime client exists. ` +
`This call indicates a DnsProviderDoc with type: 'dcrouter' was persisted, which should never happen.`,
);
}
default: { default: {
// If you see a TypeScript error here after extending TDnsProviderType, // If you see a TypeScript error here after extending TDnsProviderType,
// add a `case` for the new type above. The `never` enforces exhaustiveness. // add a `case` for the new type above. The `never` enforces exhaustiveness.

View File

@@ -46,15 +46,28 @@ export class DnsProviderHandler {
} }
private registerHandlers(): void { private registerHandlers(): void {
// Get all providers // Get all providers — prepends the built-in DcRouter pseudo-provider
// so operators see a uniform "who serves this?" list that includes the
// authoritative dcrouter alongside external accounts.
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsProviders>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsProviders>(
'getDnsProviders', 'getDnsProviders',
async (dataArg) => { async (dataArg) => {
await this.requireAuth(dataArg, 'dns-providers:read'); await this.requireAuth(dataArg, 'dns-providers:read');
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) return { providers: [] }; const synthetic: interfaces.data.IDnsProviderPublic = {
return { providers: await dnsManager.listProviders() }; id: interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID,
name: 'DcRouter',
type: 'dcrouter',
status: 'ok',
createdAt: 0,
updatedAt: 0,
createdBy: 'system',
hasCredentials: false,
builtIn: true,
};
const real = dnsManager ? await dnsManager.listProviders() : [];
return { providers: [synthetic, ...real] };
}, },
), ),
); );
@@ -78,6 +91,12 @@ export class DnsProviderHandler {
'createDnsProvider', 'createDnsProvider',
async (dataArg) => { async (dataArg) => {
const userId = await this.requireAuth(dataArg, 'dns-providers:write'); const userId = await this.requireAuth(dataArg, 'dns-providers:write');
if (dataArg.type === 'dcrouter') {
return {
success: false,
message: 'cannot create built-in provider',
};
}
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) { if (!dnsManager) {
return { success: false, message: 'DnsManager not initialized (DB disabled?)' }; return { success: false, message: 'DnsManager not initialized (DB disabled?)' };
@@ -99,6 +118,9 @@ export class DnsProviderHandler {
'updateDnsProvider', 'updateDnsProvider',
async (dataArg) => { async (dataArg) => {
await this.requireAuth(dataArg, 'dns-providers:write'); await this.requireAuth(dataArg, 'dns-providers:write');
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
return { success: false, message: 'cannot edit built-in provider' };
}
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' }; if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
const ok = await dnsManager.updateProvider(dataArg.id, { const ok = await dnsManager.updateProvider(dataArg.id, {
@@ -116,6 +138,9 @@ export class DnsProviderHandler {
'deleteDnsProvider', 'deleteDnsProvider',
async (dataArg) => { async (dataArg) => {
await this.requireAuth(dataArg, 'dns-providers:write'); await this.requireAuth(dataArg, 'dns-providers:write');
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
return { success: false, message: 'cannot delete built-in provider' };
}
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' }; if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false); return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
@@ -129,6 +154,13 @@ export class DnsProviderHandler {
'testDnsProvider', 'testDnsProvider',
async (dataArg) => { async (dataArg) => {
await this.requireAuth(dataArg, 'dns-providers:read'); await this.requireAuth(dataArg, 'dns-providers:read');
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
return {
ok: false,
error: 'built-in provider has no external connection to test',
testedAt: Date.now(),
};
}
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) { if (!dnsManager) {
return { ok: false, error: 'DnsManager not initialized', testedAt: Date.now() }; return { ok: false, error: 'DnsManager not initialized', testedAt: Date.now() };
@@ -144,6 +176,12 @@ export class DnsProviderHandler {
'listProviderDomains', 'listProviderDomains',
async (dataArg) => { async (dataArg) => {
await this.requireAuth(dataArg, 'dns-providers:read'); await this.requireAuth(dataArg, 'dns-providers:read');
if (dataArg.providerId === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
return {
success: false,
message: 'built-in provider has no external domain listing — use "Add DcRouter Domain" instead',
};
}
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' }; if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
try { try {

View File

@@ -71,7 +71,7 @@ export class DomainHandler {
), ),
); );
// Create manual domain // Create dcrouter-hosted domain
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDomain>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDomain>(
'createDomain', 'createDomain',
@@ -80,7 +80,7 @@ export class DomainHandler {
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' }; if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
try { try {
const id = await dnsManager.createManualDomain({ const id = await dnsManager.createDcrouterDomain({
name: dataArg.name, name: dataArg.name,
description: dataArg.description, description: dataArg.description,
createdBy: userId, createdBy: userId,

View File

@@ -1,9 +1,28 @@
/** /**
* Supported DNS provider types. Initially Cloudflare; the abstraction is * Stable ID for the built-in DcRouter pseudo-provider. The Providers list
* designed so additional providers (Route53, Gandi, DigitalOcean…) can be * surfaces this as the first, non-deletable row so operators see a uniform
* added by implementing the IDnsProvider class interface in ts/dns/providers/. * "who serves this?" answer for every domain. The ID is magic — it never
* exists in the DnsProviderDoc collection; handlers inject it at read time
* and reject any mutation that targets it.
*/ */
export type TDnsProviderType = 'cloudflare'; export const DCROUTER_BUILTIN_PROVIDER_ID = '__dcrouter__';
/**
* Supported DNS provider types.
*
* - 'cloudflare' → Cloudflare account (API token-based). Provider stays
* authoritative; dcrouter pushes record changes via API.
* - 'dcrouter' → Built-in pseudo-provider for dcrouter-hosted zones.
* dcrouter itself is the authoritative DNS server. No
* credentials, cannot be created/edited/deleted through
* the provider CRUD — the Providers view renders it from
* a handler-level synthetic row.
*
* The abstraction is designed so additional providers (Route53, Gandi,
* DigitalOcean, foreign dcrouters…) can be added by implementing the
* IDnsProvider class interface in ts/dns/providers/.
*/
export type TDnsProviderType = 'cloudflare' | 'dcrouter';
/** /**
* Status of the last connection test against a provider. * Status of the last connection test against a provider.
@@ -58,6 +77,12 @@ export interface IDnsProviderPublic {
createdBy: string; createdBy: string;
/** Whether credentials are configured (true after creation). Never the secret itself. */ /** Whether credentials are configured (true after creation). Never the secret itself. */
hasCredentials: boolean; hasCredentials: boolean;
/**
* True for the built-in DcRouter pseudo-provider — read-only, cannot be
* created / edited / deleted. Injected by the handler layer, never
* persisted in the DnsProviderDoc collection.
*/
builtIn?: boolean;
} }
/** /**
@@ -114,11 +139,18 @@ export interface IDnsProviderTypeDescriptor {
* credentials each one needs. Used by both backend and frontend. * credentials each one needs. Used by both backend and frontend.
*/ */
export const dnsProviderTypeDescriptors: ReadonlyArray<IDnsProviderTypeDescriptor> = [ export const dnsProviderTypeDescriptors: ReadonlyArray<IDnsProviderTypeDescriptor> = [
{
type: 'dcrouter',
displayName: 'DcRouter (built-in)',
description:
'Built-in authoritative DNS. Records are served directly by dcrouter — delegate the domain\'s NS records to make this effective.',
credentialFields: [],
},
{ {
type: 'cloudflare', type: 'cloudflare',
displayName: 'Cloudflare', displayName: 'Cloudflare',
description: description:
'Manages records via the Cloudflare API. Provider stays authoritative; dcrouter pushes record changes.', 'External DNS provider. The provider stays authoritative; dcrouter pushes record changes via its API.',
credentialFields: [ credentialFields: [
{ {
key: 'apiToken', key: 'apiToken',

View File

@@ -6,16 +6,18 @@ export type TDnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA
/** /**
* Where a DNS record came from. * Where a DNS record came from.
* *
* - 'manual' → created in the dcrouter UI / API * - 'local' originated in this dcrouter (created via UI / API)
* - 'synced' → pulled from a provider during a sync operation * - 'synced' → pulled from an upstream provider (Cloudflare, foreign
* dcrouter, …) during a sync operation
*/ */
export type TDnsRecordSource = 'manual' | 'synced'; export type TDnsRecordSource = 'local' | 'synced';
/** /**
* A DNS record. For manual (authoritative) domains, the record is registered * A DNS record. For dcrouter-hosted (authoritative) domains, the record is
* with the embedded smartdns.DnsServer. For provider-managed domains, the * registered with the embedded smartdns.DnsServer. For provider-managed
* record is mirrored from / pushed to the provider API and `providerRecordId` * domains, the record is mirrored from / pushed to the provider API and
* holds the provider's internal record id (for updates and deletes). * `providerRecordId` holds the provider's internal record id (for updates
* and deletes).
*/ */
export interface IDnsRecord { export interface IDnsRecord {
id: string; id: string;

View File

@@ -1,14 +1,15 @@
/** /**
* Where a domain came from / how it is managed. * Where a domain came from / how it is managed.
* *
* - 'manual' → operator added the domain manually. dcrouter is the * - 'dcrouter' → dcrouter is the authoritative DNS server for this domain;
* authoritative DNS server for it; records are served by * records are served by the embedded smartdns.DnsServer.
* the embedded smartdns.DnsServer. * Operators delegate the domain's NS records to make this
* effective.
* - 'provider' → domain was imported from an external DNS provider * - 'provider' → domain was imported from an external DNS provider
* (e.g. Cloudflare). The provider stays authoritative; * (e.g. Cloudflare). The provider stays authoritative;
* dcrouter only reads/writes records via the provider API. * dcrouter only reads/writes records via the provider API.
*/ */
export type TDomainSource = 'manual' | 'provider'; export type TDomainSource = 'dcrouter' | 'provider';
/** /**
* A domain under management by dcrouter. * A domain under management by dcrouter.
@@ -20,7 +21,7 @@ export interface IDomain {
source: TDomainSource; source: TDomainSource;
/** ID of the DnsProvider that owns this domain — only set when source === 'provider'. */ /** ID of the DnsProvider that owns this domain — only set when source === 'provider'. */
providerId?: string; providerId?: string;
/** True when dcrouter is the authoritative DNS server for this domain (source === 'manual'). */ /** True when dcrouter is the authoritative DNS server for this domain (source === 'dcrouter'). */
authoritative: boolean; authoritative: boolean;
/** Authoritative nameservers (display only — populated from provider for imported domains). */ /** Authoritative nameservers (display only — populated from provider for imported domains). */
nameservers?: string[]; nameservers?: string[];

View File

@@ -45,7 +45,7 @@ export interface IReq_GetDnsRecord extends plugins.typedrequestInterfaces.implem
/** /**
* Create a new DNS record. * Create a new DNS record.
* *
* For manual domains: registers the record with the embedded DnsServer. * For dcrouter-hosted domains: registers the record with the embedded DnsServer.
* For provider domains: pushes the record to the provider API. * For provider domains: pushes the record to the provider API.
*/ */
export interface IReq_CreateDnsRecord extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_CreateDnsRecord extends plugins.typedrequestInterfaces.implementsTR<

View File

@@ -42,8 +42,8 @@ export interface IReq_GetDomain extends plugins.typedrequestInterfaces.implement
} }
/** /**
* Create a manual (authoritative) domain. dcrouter will serve DNS * Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
* records for this domain via the embedded smartdns.DnsServer. * DNS records for this domain via the embedded smartdns.DnsServer.
*/ */
export interface IReq_CreateDomain extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_CreateDomain extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,
@@ -130,7 +130,7 @@ export interface IReq_DeleteDomain extends plugins.typedrequestInterfaces.implem
/** /**
* Force-resync a provider-managed domain: re-pulls all records from the * Force-resync a provider-managed domain: re-pulls all records from the
* provider API, replacing the cached DnsRecordDocs. * provider API, replacing the cached DnsRecordDocs.
* No-op for manual domains. * No-op for dcrouter-hosted domains.
*/ */
export interface IReq_SyncDomain extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_SyncDomain extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,

View File

@@ -64,6 +64,34 @@ export async function createMigrationRunner(
migrated++; migrated++;
} }
ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`); ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`);
})
.step('rename-domain-source-manual-to-dcrouter')
.from('13.1.0').to('13.8.1')
.description('Rename DomainDoc.source value from "manual" to "dcrouter"')
.up(async (ctx) => {
const collection = ctx.mongo!.collection('domaindoc');
const result = await collection.updateMany(
{ source: 'manual' },
{ $set: { source: 'dcrouter' } },
);
ctx.log.log(
'info',
`rename-domain-source-manual-to-dcrouter: migrated ${result.modifiedCount} domain(s)`,
);
})
.step('rename-record-source-manual-to-local')
.from('13.8.1').to('13.8.2')
.description('Rename DnsRecordDoc.source value from "manual" to "local"')
.up(async (ctx) => {
const collection = ctx.mongo!.collection('dnsrecorddoc');
const result = await collection.updateMany(
{ source: 'manual' },
{ $set: { source: 'local' } },
);
ctx.log.log(
'info',
`rename-record-source-manual-to-local: migrated ${result.modifiedCount} record(s)`,
);
}); });
return migration; return migration;

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '13.8.0', version: '13.9.2',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -1793,7 +1793,7 @@ export async function fetchProviderDomains(
return await request.fire({ identity: context.identity, providerId }); return await request.fire({ identity: context.identity, providerId });
} }
export const createManualDomainAction = domainsStatePart.createAction<{ export const createDcrouterDomainAction = domainsStatePart.createAction<{
name: string; name: string;
description?: string; description?: string;
}>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => { }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {

View File

@@ -222,7 +222,7 @@ export class OpsViewApiTokens extends DeesElement {
.suggestions=${allScopes} .suggestions=${allScopes}
.required=${true} .required=${true}
></dees-input-tags> ></dees-input-tags>
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in (days, blank = never)'}></dees-input-text> <dees-input-text .key=${'expiresInDays'} .label=${'Expires in'} .description=${'Number of days; leave blank for no expiration'}></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [

View File

@@ -44,12 +44,15 @@ export class DnsProviderForm extends DeesElement {
accessor providerName: string = ''; accessor providerName: string = '';
/** /**
* Currently selected provider type. Initialized to the first descriptor; * Currently selected provider type. Initialized to the first user-creatable
* caller can override before mounting (e.g. for edit dialogs). * descriptor; caller can override before mounting (e.g. for edit dialogs).
* The built-in 'dcrouter' pseudo-provider is excluded from the picker —
* operators cannot create another one.
*/ */
@state() @state()
accessor selectedType: interfaces.data.TDnsProviderType = accessor selectedType: interfaces.data.TDnsProviderType =
interfaces.data.dnsProviderTypeDescriptors[0]?.type ?? 'cloudflare'; interfaces.data.dnsProviderTypeDescriptors.find((d) => d.type !== 'dcrouter')?.type ??
'cloudflare';
/** When true, hide the type picker — used in edit dialogs. */ /** When true, hide the type picker — used in edit dialogs. */
@property({ type: Boolean }) @property({ type: Boolean })
@@ -77,22 +80,6 @@ export class DnsProviderForm extends DeesElement {
margin-bottom: 12px; margin-bottom: 12px;
} }
.helpText {
font-size: 12px;
opacity: 0.7;
margin-top: -6px;
margin-bottom: 8px;
}
.typeDescription {
font-size: 12px;
opacity: 0.8;
margin: 4px 0 16px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
border-radius: 6px;
}
.credentialsHint { .credentialsHint {
font-size: 12px; font-size: 12px;
opacity: 0.7; opacity: 0.7;
@@ -102,7 +89,12 @@ export class DnsProviderForm extends DeesElement {
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const descriptors = interfaces.data.dnsProviderTypeDescriptors; // Exclude the built-in 'dcrouter' pseudo-provider from the type picker —
// operators cannot create another one, it's surfaced at read time by the
// backend handler instead.
const descriptors = interfaces.data.dnsProviderTypeDescriptors.filter(
(d) => d.type !== 'dcrouter',
);
const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType); const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
return html` return html`
@@ -122,6 +114,7 @@ export class DnsProviderForm extends DeesElement {
<dees-input-text <dees-input-text
.key=${'__type_display'} .key=${'__type_display'}
.label=${'Type'} .label=${'Type'}
.infoText=${descriptor?.description || ''}
.value=${descriptor?.displayName ?? this.selectedType} .value=${descriptor?.displayName ?? this.selectedType}
.disabled=${true} .disabled=${true}
></dees-input-text> ></dees-input-text>
@@ -132,6 +125,7 @@ export class DnsProviderForm extends DeesElement {
<dees-input-dropdown <dees-input-dropdown
.key=${'__type'} .key=${'__type'}
.label=${'Provider type'} .label=${'Provider type'}
.infoText=${descriptor?.description || ''}
.options=${descriptors.map((d) => ({ option: d.displayName, key: d.type }))} .options=${descriptors.map((d) => ({ option: d.displayName, key: d.type }))}
.selectedOption=${descriptor .selectedOption=${descriptor
? { option: descriptor.displayName, key: descriptor.type } ? { option: descriptor.displayName, key: descriptor.type }
@@ -150,7 +144,6 @@ export class DnsProviderForm extends DeesElement {
`} `}
${descriptor ${descriptor
? html` ? html`
<div class="typeDescription">${descriptor.description}</div>
${this.credentialsHint ${this.credentialsHint
? html`<div class="credentialsHint">${this.credentialsHint}</div>` ? html`<div class="credentialsHint">${this.credentialsHint}</div>`
: ''} : ''}
@@ -160,9 +153,9 @@ export class DnsProviderForm extends DeesElement {
<dees-input-text <dees-input-text
.key=${f.key} .key=${f.key}
.label=${f.label} .label=${f.label}
.description=${f.helpText || ''}
.required=${f.required && !this.lockType} .required=${f.required && !this.lockType}
></dees-input-text> ></dees-input-text>
${f.helpText ? html`<div class="helpText">${f.helpText}</div>` : ''}
</div> </div>
`, `,
)} )}

View File

@@ -54,35 +54,31 @@ export class OpsViewCertificates extends DeesElement {
gap: 24px; gap: 24px;
} }
.acmeCard { .acmeTileHeader {
padding: 16px 20px;
background: ${cssManager.bdTheme('#f9fafb', '#111827')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 8px;
}
.acmeCard.acmeCardEmpty {
background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
border-color: ${cssManager.bdTheme('#fde68a', '#78350f')};
}
.acmeCardHeader {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; width: 100%;
padding: 8px 12px;
} }
.acmeCardTitle { .acmeTileHeading {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#111827', '#f3f4f6')}; }
.acmeEmptyContent {
padding: 16px;
font-size: 13px;
line-height: 1.5;
color: ${cssManager.bdTheme('#78350f', '#fde68a')};
} }
.acmeGrid { .acmeGrid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px 24px; gap: 12px 24px;
padding: 16px;
} }
.acmeField { .acmeField {
@@ -103,13 +99,6 @@ export class OpsViewCertificates extends DeesElement {
color: ${cssManager.bdTheme('#111827', '#f3f4f6')}; color: ${cssManager.bdTheme('#111827', '#f3f4f6')};
} }
.acmeEmptyHint {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: ${cssManager.bdTheme('#78350f', '#fde68a')};
}
.statusBadge { .statusBadge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -226,41 +215,40 @@ export class OpsViewCertificates extends DeesElement {
<dees-heading level="3">Certificates</dees-heading> <dees-heading level="3">Certificates</dees-heading>
<div class="certificatesContainer"> <div class="certificatesContainer">
${this.renderAcmeSettingsCard()}
${this.renderStatsTiles(summary)} ${this.renderStatsTiles(summary)}
${this.renderAcmeSettingsTile()}
${this.renderCertificateTable()} ${this.renderCertificateTable()}
</div> </div>
`; `;
} }
private renderAcmeSettingsCard(): TemplateResult { private renderAcmeSettingsTile(): TemplateResult {
const config = this.acmeState.config; const config = this.acmeState.config;
if (!config) { if (!config) {
return html` return html`
<div class="acmeCard acmeCardEmpty"> <dees-tile .heading=${'ACME Settings'}>
<div class="acmeCardHeader"> <div slot="header" class="acmeTileHeader">
<span class="acmeCardTitle">ACME Settings</span> <span class="acmeTileHeading">ACME Settings</span>
<dees-button <dees-button
eventName="edit-acme"
@click=${() => this.showEditAcmeDialog()} @click=${() => this.showEditAcmeDialog()}
.type=${'highlighted'} .type=${'highlighted'}
>Configure</dees-button> >Configure</dees-button>
</div> </div>
<p class="acmeEmptyHint"> <div class="acmeEmptyContent">
No ACME configuration yet. Click <strong>Configure</strong> to set up automated TLS No ACME configuration yet. Click <strong>Configure</strong> to set up automated TLS
certificate issuance via Let's Encrypt. You'll also need at least one DNS provider certificate issuance via Let's Encrypt. You'll also need at least one DNS provider
under <strong>Domains &gt; Providers</strong>. under <strong>Domains &gt; Providers</strong>.
</p>
</div> </div>
</dees-tile>
`; `;
} }
return html` return html`
<div class="acmeCard"> <dees-tile>
<div class="acmeCardHeader"> <div slot="header" class="acmeTileHeader">
<span class="acmeCardTitle">ACME Settings</span> <span class="acmeTileHeading">ACME Settings</span>
<dees-button eventName="edit-acme" @click=${() => this.showEditAcmeDialog()}>Edit</dees-button> <dees-button @click=${() => this.showEditAcmeDialog()}>Edit</dees-button>
</div> </div>
<div class="acmeGrid"> <div class="acmeGrid">
<div class="acmeField"> <div class="acmeField">
@@ -292,7 +280,7 @@ export class OpsViewCertificates extends DeesElement {
<span class="acmeValue">${config.renewThresholdDays} days</span> <span class="acmeValue">${config.renewThresholdDays} days</span>
</div> </div>
</div> </div>
</div> </dees-tile>
`; `;
} }
@@ -317,7 +305,8 @@ export class OpsViewCertificates extends DeesElement {
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.key=${'useProduction'} .key=${'useProduction'}
.label=${"Use Let's Encrypt production (uncheck for staging)"} .label=${"Use Let's Encrypt production"}
.description=${'Uncheck to use the staging environment'}
.value=${current?.useProduction ?? true} .value=${current?.useProduction ?? true}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
@@ -327,7 +316,8 @@ export class OpsViewCertificates extends DeesElement {
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-text <dees-input-text
.key=${'renewThresholdDays'} .key=${'renewThresholdDays'}
.label=${'Renewal threshold (days)'} .label=${'Renewal threshold'}
.description=${'Number of days before expiry to trigger renewal'}
.value=${String(current?.renewThresholdDays ?? 30)} .value=${String(current?.renewThresholdDays ?? 30)}
></dees-input-text> ></dees-input-text>
</dees-form> </dees-form>
@@ -456,11 +446,12 @@ export class OpsViewCertificates extends DeesElement {
content: html` content: html`
<dees-form> <dees-form>
<dees-input-fileupload <dees-input-fileupload
key="certJsonFile" .key=${'certJsonFile'}
label="Certificate JSON (.tsclass.cert.json)" .label=${'Certificate JSON'}
accept=".json" .description=${'Upload a .tsclass.cert.json file'}
.accept=${'.json'}
.multiple=${false} .multiple=${false}
required .required=${true}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-form> </dees-form>
`, `,

View File

@@ -80,7 +80,7 @@ export class OpsViewDns extends DeesElement {
font-weight: 500; font-weight: 500;
} }
.sourceBadge.manual { .sourceBadge.local {
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')}; background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')}; color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
} }
@@ -101,8 +101,8 @@ export class OpsViewDns extends DeesElement {
<dees-heading level="3">DNS Records</dees-heading> <dees-heading level="3">DNS Records</dees-heading>
<div class="dnsContainer"> <div class="dnsContainer">
<div class="domainPicker"> <div class="domainPicker">
<span>Domain:</span>
<dees-input-dropdown <dees-input-dropdown
.label=${'Domain'}
.options=${domains.map((d) => ({ option: d.name, key: d.id }))} .options=${domains.map((d) => ({ option: d.name, key: d.id }))}
.selectedOption=${selectedId .selectedOption=${selectedId
? { option: domains.find((d) => d.id === selectedId)?.name || '', key: selectedId } ? { option: domains.find((d) => d.id === selectedId)?.name || '', key: selectedId }
@@ -184,7 +184,7 @@ export class OpsViewDns extends DeesElement {
private domainHint(domainId: string): string { private domainHint(domainId: string): string {
const domain = this.domainsState.domains.find((d) => d.id === domainId); const domain = this.domainsState.domains.find((d) => d.id === domainId);
if (!domain) return ''; if (!domain) return '';
if (domain.source === 'manual') { if (domain.source === 'dcrouter') {
return 'Records are served by dcrouter (authoritative).'; return 'Records are served by dcrouter (authoritative).';
} }
return 'Records are stored at the provider — changes here are pushed via the provider API.'; return 'Records are stored at the provider — changes here are pushed via the provider API.';
@@ -196,7 +196,7 @@ export class OpsViewDns extends DeesElement {
heading: 'Add DNS Record', heading: 'Add DNS Record',
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Name (FQDN)'} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Name'} .description=${'Fully qualified domain name'} .required=${true}></dees-input-text>
<dees-input-dropdown <dees-input-dropdown
.key=${'type'} .key=${'type'}
.label=${'Type'} .label=${'Type'}
@@ -205,10 +205,11 @@ export class OpsViewDns extends DeesElement {
></dees-input-dropdown> ></dees-input-dropdown>
<dees-input-text <dees-input-text
.key=${'value'} .key=${'value'}
.label=${'Value (for MX use "10 mail.example.com")'} .label=${'Value'}
.description=${'For MX records use priority format, e.g. "10 mail.example.com"'}
.required=${true} .required=${true}
></dees-input-text> ></dees-input-text>
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${'300'}></dees-input-text> <dees-input-text .key=${'ttl'} .label=${'TTL'} .description=${'Time to live in seconds'} .value=${'300'}></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [
@@ -242,9 +243,9 @@ export class OpsViewDns extends DeesElement {
heading: `Edit ${rec.type} ${rec.name}`, heading: `Edit ${rec.type} ${rec.name}`,
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Name (FQDN)'} .value=${rec.name}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Name'} .description=${'Fully qualified domain name'} .value=${rec.name}></dees-input-text>
<dees-input-text .key=${'value'} .label=${'Value'} .value=${rec.value}></dees-input-text> <dees-input-text .key=${'value'} .label=${'Value'} .value=${rec.value}></dees-input-text>
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${String(rec.ttl)}></dees-input-text> <dees-input-text .key=${'ttl'} .label=${'TTL'} .description=${'Time to live in seconds'} .value=${String(rec.ttl)}></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [

View File

@@ -55,7 +55,7 @@ export class OpsViewDomains extends DeesElement {
font-weight: 500; font-weight: 500;
} }
.sourceBadge.manual { .sourceBadge.dcrouter {
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')}; background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')}; color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
} }
@@ -76,7 +76,7 @@ export class OpsViewDomains extends DeesElement {
<div class="domainsContainer"> <div class="domainsContainer">
<dees-table <dees-table
.heading1=${'Domains'} .heading1=${'Domains'}
.heading2=${'Domains under management — manual (authoritative) or imported from a provider'} .heading2=${'Domains under management — served by dcrouter (authoritative) or imported from a provider'}
.data=${domains} .data=${domains}
.showColumnFilters=${true} .showColumnFilters=${true}
.displayFunction=${(d: interfaces.data.IDomain) => ({ .displayFunction=${(d: interfaces.data.IDomain) => ({
@@ -90,11 +90,11 @@ export class OpsViewDomains extends DeesElement {
})} })}
.dataActions=${[ .dataActions=${[
{ {
name: 'Add Manual Domain', name: 'Add DcRouter Domain',
iconName: 'lucide:plus', iconName: 'lucide:plus',
type: ['header' as const], type: ['header' as const],
actionFunc: async () => { actionFunc: async () => {
await this.showCreateManualDialog(); await this.showCreateDcrouterDialog();
}, },
}, },
{ {
@@ -168,21 +168,21 @@ export class OpsViewDomains extends DeesElement {
d: interfaces.data.IDomain, d: interfaces.data.IDomain,
providersById: Map<string, interfaces.data.IDnsProviderPublic>, providersById: Map<string, interfaces.data.IDnsProviderPublic>,
): TemplateResult { ): TemplateResult {
if (d.source === 'manual') { if (d.source === 'dcrouter') {
return html`<span class="sourceBadge manual">Manual</span>`; return html`<span class="sourceBadge dcrouter">DcRouter</span>`;
} }
const provider = d.providerId ? providersById.get(d.providerId) : undefined; const provider = d.providerId ? providersById.get(d.providerId) : undefined;
return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`; return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`;
} }
private async showCreateManualDialog() { private async showCreateDcrouterDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog'); const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({ DeesModal.createAndShow({
heading: 'Add Manual Domain', heading: 'Add DcRouter Domain',
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'FQDN (e.g. example.com)'} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'FQDN'} .description=${'e.g. example.com'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description (optional)'}></dees-input-text> <dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
</dees-form> </dees-form>
<p style="margin-top: 12px; font-size: 12px; opacity: 0.7;"> <p style="margin-top: 12px; font-size: 12px; opacity: 0.7;">
dcrouter will become the authoritative DNS server for this domain. You'll need to dcrouter will become the authoritative DNS server for this domain. You'll need to
@@ -199,7 +199,7 @@ export class OpsViewDomains extends DeesElement {
?.querySelector('dees-form'); ?.querySelector('dees-form');
if (!form) return; if (!form) return;
const data = await form.collectFormData(); const data = await form.collectFormData();
await appstate.domainsStatePart.dispatchAction(appstate.createManualDomainAction, { await appstate.domainsStatePart.dispatchAction(appstate.createDcrouterDomainAction, {
name: String(data.name), name: String(data.name),
description: data.description ? String(data.description) : undefined, description: data.description ? String(data.description) : undefined,
}); });
@@ -235,7 +235,8 @@ export class OpsViewDomains extends DeesElement {
></dees-input-dropdown> ></dees-input-dropdown>
<dees-input-text <dees-input-text
.key=${'domainNames'} .key=${'domainNames'}
.label=${'Comma-separated FQDNs to import (e.g. example.com, foo.com)'} .label=${'Domain Names'}
.description=${'Comma-separated FQDNs, e.g. example.com, foo.com'}
.required=${true} .required=${true}
></dees-input-text> ></dees-input-text>
</dees-form> </dees-form>

View File

@@ -71,6 +71,11 @@ export class OpsViewProviders extends DeesElement {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')}; background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')}; color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
} }
.statusBadge.builtin {
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
}
`, `,
]; ];
@@ -82,15 +87,21 @@ export class OpsViewProviders extends DeesElement {
<div class="providersContainer"> <div class="providersContainer">
<dees-table <dees-table
.heading1=${'Providers'} .heading1=${'Providers'}
.heading2=${'External DNS provider accounts'} .heading2=${'Built-in dcrouter + external DNS provider accounts'}
.data=${providers} .data=${providers}
.showColumnFilters=${true} .showColumnFilters=${true}
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({ .displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
Name: p.name, Name: p.name,
Type: this.providerTypeLabel(p.type), Type: this.providerTypeLabel(p.type),
Status: this.renderStatusBadge(p.status), Status: p.builtIn
'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never', ? html`<span class="statusBadge builtin">built-in</span>`
Error: p.lastError || '-', : this.renderStatusBadge(p.status),
'Last Tested': p.builtIn
? '—'
: p.lastTestedAt
? new Date(p.lastTestedAt).toLocaleString()
: 'never',
Error: p.builtIn ? '—' : p.lastError || '-',
})} })}
.dataActions=${[ .dataActions=${[
{ {
@@ -116,6 +127,7 @@ export class OpsViewProviders extends DeesElement {
name: 'Test Connection', name: 'Test Connection',
iconName: 'lucide:plug', iconName: 'lucide:plug',
type: ['inRow', 'contextmenu'] as any, type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
actionFunc: async (actionData: any) => { actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic; const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.testProvider(provider); await this.testProvider(provider);
@@ -125,6 +137,7 @@ export class OpsViewProviders extends DeesElement {
name: 'Edit', name: 'Edit',
iconName: 'lucide:pencil', iconName: 'lucide:pencil',
type: ['inRow', 'contextmenu'] as any, type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
actionFunc: async (actionData: any) => { actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic; const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.showEditDialog(provider); await this.showEditDialog(provider);
@@ -134,6 +147,7 @@ export class OpsViewProviders extends DeesElement {
name: 'Delete', name: 'Delete',
iconName: 'lucide:trash2', iconName: 'lucide:trash2',
type: ['inRow', 'contextmenu'] as any, type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
actionFunc: async (actionData: any) => { actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic; const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.deleteProvider(provider); await this.deleteProvider(provider);

View File

@@ -323,6 +323,8 @@ export class OpsViewNetworkActivity extends DeesElement {
<!-- Requests Table --> <!-- Requests Table -->
<dees-table <dees-table
.data=${this.networkRequests} .data=${this.networkRequests}
.rowKey=${'id'}
.highlightUpdates=${'flash'}
.displayFunction=${(req: INetworkRequest) => ({ .displayFunction=${(req: INetworkRequest) => ({
Time: new Date(req.timestamp).toLocaleTimeString(), Time: new Date(req.timestamp).toLocaleTimeString(),
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`, Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
@@ -595,6 +597,8 @@ export class OpsViewNetworkActivity extends DeesElement {
return html` return html`
<dees-table <dees-table
.data=${this.networkState.topIPs} .data=${this.networkState.topIPs}
.rowKey=${'ip'}
.highlightUpdates=${'flash'}
.displayFunction=${(ipData: { ip: string; count: number }) => { .displayFunction=${(ipData: { ip: string; count: number }) => {
const bw = bandwidthByIP.get(ipData.ip); const bw = bandwidthByIP.get(ipData.ip);
return { return {
@@ -624,6 +628,8 @@ export class OpsViewNetworkActivity extends DeesElement {
return html` return html`
<dees-table <dees-table
.data=${backends} .data=${backends}
.rowKey=${'backend'}
.highlightUpdates=${'flash'}
.displayFunction=${(item: interfaces.data.IBackendInfo) => { .displayFunction=${(item: interfaces.data.IBackendInfo) => {
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors; const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
const protocolClass = item.protocol.toLowerCase().replace(/[^a-z0-9]/g, ''); const protocolClass = item.protocol.toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -724,19 +730,10 @@ export class OpsViewNetworkActivity extends DeesElement {
this.requestsPerSecHistory.shift(); this.requestsPerSecHistory.shift();
} }
// Only update if connections changed significantly // Reassign unconditionally so dees-table's flash diff can compare per-cell
const newConnectionCount = this.networkState.connections.length; // values against the previous snapshot. Row identity is preserved via
const oldConnectionCount = this.networkRequests.length; // rowKey='id', so DOM nodes are reused across ticks.
this.networkRequests = this.networkState.connections.map((conn) => ({
// Check if we need to update the network requests array
const shouldUpdate = newConnectionCount !== oldConnectionCount ||
newConnectionCount === 0 ||
(newConnectionCount > 0 && this.networkRequests.length === 0);
if (shouldUpdate) {
// Convert connection data to network requests format
if (newConnectionCount > 0) {
this.networkRequests = this.networkState.connections.map((conn, index) => ({
id: conn.id, id: conn.id,
timestamp: conn.startTime, timestamp: conn.startTime,
method: 'GET', // Default method for proxy connections method: 'GET', // Default method for proxy connections
@@ -751,10 +748,6 @@ export class OpsViewNetworkActivity extends DeesElement {
remoteIp: conn.remoteAddress, remoteIp: conn.remoteAddress,
route: 'proxy', route: 'proxy',
})); }));
} else {
this.networkRequests = [];
}
}
// Load server-side throughput history into chart (once) // Load server-side throughput history into chart (once)
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) { if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {

View File

@@ -220,6 +220,8 @@ export class OpsViewRemoteIngress extends DeesElement {
.heading1=${'Edge Nodes'} .heading1=${'Edge Nodes'}
.heading2=${'Manage remote ingress edge registrations'} .heading2=${'Manage remote ingress edge registrations'}
.data=${this.riState.edges} .data=${this.riState.edges}
.rowKey=${'id'}
.highlightUpdates=${'flash'}
.showColumnFilters=${true} .showColumnFilters=${true}
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({ .displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
name: edge.name, name: edge.name,
@@ -241,9 +243,9 @@ export class OpsViewRemoteIngress extends DeesElement {
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'listenPorts'} .label=${'Additional Manual Ports (comma-separated, optional)'}></dees-input-text> <dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers, optional'}></dees-input-text>
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox> <dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text> <dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated, optional'}></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [
@@ -318,9 +320,9 @@ export class OpsViewRemoteIngress extends DeesElement {
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text>
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports (comma-separated)'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text> <dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox> <dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated)'} .value=${(edge.tags || []).join(', ')}></dees-input-text> <dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [

View File

@@ -473,19 +473,19 @@ export class OpsViewRoutes extends DeesElement {
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Route Name'} .value=${route.name || ''} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Route Name'} .value=${route.name || ''} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .value=${currentPorts} .required=${true}></dees-input-text> <dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .value=${currentPorts} .required=${true}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list> <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text> <dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
<dees-input-dropdown .key=${'sourceProfileRef'} .label=${'Source Profile'} .options=${profileOptions} .selectedOption=${profileOptions.find((o) => o.key === (merged.metadata?.sourceProfileRef || '')) || null}></dees-input-dropdown> <dees-input-dropdown .key=${'sourceProfileRef'} .label=${'Source Profile'} .options=${profileOptions} .selectedOption=${profileOptions.find((o) => o.key === (merged.metadata?.sourceProfileRef || '')) || null}></dees-input-dropdown>
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown> <dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${currentTargetHost}></dees-input-text> <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></dees-input-text> <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
<dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions.find((o) => o.key === currentTlsMode) || tlsModeOptions[0]}></dees-input-dropdown> <dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions.find((o) => o.key === currentTlsMode) || tlsModeOptions[0]}></dees-input-dropdown>
<div class="tlsCertificateGroup" style="display: ${needsCert ? 'flex' : 'none'}; flex-direction: column; gap: 16px;"> <div class="tlsCertificateGroup" style="display: ${needsCert ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
<dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions.find((o) => o.key === currentTlsCert) || tlsCertOptions[0]}></dees-input-dropdown> <dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions.find((o) => o.key === currentTlsCert) || tlsCertOptions[0]}></dees-input-dropdown>
<div class="tlsCustomCertGroup" style="display: ${needsCert && isCustom ? 'flex' : 'none'}; flex-direction: column; gap: 16px;"> <div class="tlsCustomCertGroup" style="display: ${needsCert && isCustom ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
<dees-input-text .key=${'tlsCertKey'} .label=${'Private Key (PEM)'} .value=${currentCustomKey}></dees-input-text> <dees-input-text .key=${'tlsCertKey'} .label=${'Private Key'} .description=${'PEM-encoded private key'} .value=${currentCustomKey}></dees-input-text>
<dees-input-text .key=${'tlsCertCert'} .label=${'Certificate (PEM)'} .value=${currentCustomCert}></dees-input-text> <dees-input-text .key=${'tlsCertCert'} .label=${'Certificate'} .description=${'PEM-encoded certificate'} .value=${currentCustomCert}></dees-input-text>
</div> </div>
</div> </div>
</dees-form> </dees-form>
@@ -607,19 +607,19 @@ export class OpsViewRoutes extends DeesElement {
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text> <dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .required=${true}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list> <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'}></dees-input-text> <dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'}></dees-input-text>
<dees-input-dropdown .key=${'sourceProfileRef'} .label=${'Source Profile'} .options=${profileOptions}></dees-input-dropdown> <dees-input-dropdown .key=${'sourceProfileRef'} .label=${'Source Profile'} .options=${profileOptions}></dees-input-dropdown>
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown> <dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text> <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'}></dees-input-text> <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
<dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions[0]}></dees-input-dropdown> <dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions[0]}></dees-input-dropdown>
<div class="tlsCertificateGroup" style="display: none; flex-direction: column; gap: 16px;"> <div class="tlsCertificateGroup" style="display: none; flex-direction: column; gap: 16px;">
<dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions[0]}></dees-input-dropdown> <dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions[0]}></dees-input-dropdown>
<div class="tlsCustomCertGroup" style="display: none; flex-direction: column; gap: 16px;"> <div class="tlsCustomCertGroup" style="display: none; flex-direction: column; gap: 16px;">
<dees-input-text .key=${'tlsCertKey'} .label=${'Private Key (PEM)'}></dees-input-text> <dees-input-text .key=${'tlsCertKey'} .label=${'Private Key'} .description=${'PEM-encoded private key'}></dees-input-text>
<dees-input-text .key=${'tlsCertCert'} .label=${'Certificate (PEM)'}></dees-input-text> <dees-input-text .key=${'tlsCertCert'} .label=${'Certificate'} .description=${'PEM-encoded certificate'}></dees-input-text>
</div> </div>
</div> </div>
</dees-form> </dees-form>

View File

@@ -176,7 +176,7 @@ export class OpsViewTargetProfiles extends DeesElement {
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text> <dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list> <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list>
<dees-input-list .key=${'targets'} .label=${'Targets (ip:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list> <dees-input-list .key=${'targets'} .label=${'Targets'} .description=${'Format: ip:port, e.g. 10.0.0.1:443'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list>
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true}></dees-input-list> <dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true}></dees-input-list>
</dees-form> </dees-form>
`, `,
@@ -235,7 +235,7 @@ export class OpsViewTargetProfiles extends DeesElement {
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text> <dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list> <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list>
<dees-input-list .key=${'targets'} .label=${'Targets (ip:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list> <dees-input-list .key=${'targets'} .label=${'Targets'} .description=${'Format: ip:port, e.g. 10.0.0.1:443'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list>
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true} .value=${currentRouteRefs}></dees-input-list> <dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true} .value=${currentRouteRefs}></dees-input-list>
</dees-form> </dees-form>
`, `,

View File

@@ -305,6 +305,8 @@ export class OpsViewVpn extends DeesElement {
.heading1=${'VPN Clients'} .heading1=${'VPN Clients'}
.heading2=${'Manage WireGuard and SmartVPN client registrations'} .heading2=${'Manage WireGuard and SmartVPN client registrations'}
.data=${clients} .data=${clients}
.rowKey=${'clientId'}
.highlightUpdates=${'flash'}
.showColumnFilters=${true} .showColumnFilters=${true}
.displayFunction=${(client: interfaces.data.IVpnClient) => { .displayFunction=${(client: interfaces.data.IVpnClient) => {
const conn = this.getConnectedInfo(client); const conn = this.getConnectedInfo(client);
@@ -369,8 +371,8 @@ export class OpsViewVpn extends DeesElement {
</div> </div>
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${false}></dees-input-checkbox> <dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${false}></dees-input-checkbox>
<div class="aclGroup" style="display: none; flex-direction: column; gap: 16px;"> <div class="aclGroup" style="display: none; flex-direction: column; gap: 16px;">
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'}></dees-input-text> <dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List'} .description=${'Comma-separated IPs or CIDRs'}></dees-input-text>
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'}></dees-input-text> <dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List'} .description=${'Comma-separated IPs or CIDRs'}></dees-input-text>
</div> </div>
</dees-form> </dees-form>
`, `,
@@ -679,8 +681,8 @@ export class OpsViewVpn extends DeesElement {
</div> </div>
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${currentAllowAcls}></dees-input-checkbox> <dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${currentAllowAcls}></dees-input-checkbox>
<div class="aclGroup" style="display: ${currentAllowAcls ? 'flex' : 'none'}; flex-direction: column; gap: 16px;"> <div class="aclGroup" style="display: ${currentAllowAcls ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'} .value=${currentAllowList}></dees-input-text> <dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List'} .description=${'Comma-separated IPs or CIDRs'} .value=${currentAllowList}></dees-input-text>
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'} .value=${currentBlockList}></dees-input-text> <dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List'} .description=${'Comma-separated IPs or CIDRs'} .value=${currentBlockList}></dees-input-text>
</div> </div>
</dees-form> </dees-form>
`, `,