From 55f5465a9a127603d1783da564f23b289a98c240 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 2 Apr 2026 17:27:05 +0000 Subject: [PATCH] fix(routes): support profile and target metadata in route creation and refresh remote ingress routes after config initialization --- changelog.md | 9 +++ package.json | 6 +- pnpm-lock.yaml | 48 ++++++++++------ ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 7 +++ ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 4 ++ ts_web/elements/ops-view-routes.ts | 60 ++++++++++++++++++-- ts_web/elements/ops-view-securityprofiles.ts | 28 ++++----- 9 files changed, 121 insertions(+), 45 deletions(-) diff --git a/changelog.md b/changelog.md index a5fb45e..e1e319b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-04-02 - 12.2.4 - fix(routes) +support profile and target metadata in route creation and refresh remote ingress routes after config initialization + +- Re-applies routes to the remote ingress manager after config managers finish to avoid missing DB-backed routes during initialization +- Fetches profiles and targets when opening or authenticating into the routes view so route creation dropdowns are populated +- Includes selected security profile and network target metadata when creating programmatic routes and displays that metadata in route details +- Improves security profile forms by switching IP allow/block lists to list inputs instead of comma-separated text fields +- Updates UI dependencies including smartdb, dees-catalog, and serve.zone catalog + ## 2026-04-02 - 12.2.3 - fix(repo) no changes to commit diff --git a/package.json b/package.json index 8414fbc..3d2f7b5 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,14 @@ "@api.global/typedserver": "^8.4.6", "@api.global/typedsocket": "^4.1.2", "@apiclient.xyz/cloudflare": "^7.1.0", - "@design.estate/dees-catalog": "^3.49.1", + "@design.estate/dees-catalog": "^3.49.2", "@design.estate/dees-element": "^2.2.4", "@push.rocks/lik": "^6.4.0", "@push.rocks/projectinfo": "^5.1.0", "@push.rocks/qenv": "^6.1.3", "@push.rocks/smartacme": "^9.3.1", "@push.rocks/smartdata": "^7.1.3", - "@push.rocks/smartdb": "^2.0.0", + "@push.rocks/smartdb": "^2.1.1", "@push.rocks/smartdns": "^7.9.0", "@push.rocks/smartfs": "^1.5.0", "@push.rocks/smartguard": "^3.1.0", @@ -61,7 +61,7 @@ "@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartvpn": "1.19.1", "@push.rocks/taskbuffer": "^8.0.2", - "@serve.zone/catalog": "^2.9.0", + "@serve.zone/catalog": "^2.9.1", "@serve.zone/interfaces": "^5.3.0", "@serve.zone/remoteingress": "^4.15.3", "@tsclass/tsclass": "^9.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38bb16c..be0b694 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^7.1.0 version: 7.1.0 '@design.estate/dees-catalog': - specifier: ^3.49.1 - version: 3.49.1(@tiptap/pm@2.27.2) + specifier: ^3.49.2 + version: 3.49.2(@tiptap/pm@2.27.2) '@design.estate/dees-element': specifier: ^2.2.4 version: 2.2.4 @@ -45,8 +45,8 @@ importers: specifier: ^7.1.3 version: 7.1.3(socks@2.8.7) '@push.rocks/smartdb': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.1.1 + version: 2.1.1(@tiptap/pm@2.27.2) '@push.rocks/smartdns': specifier: ^7.9.0 version: 7.9.0 @@ -102,8 +102,8 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@serve.zone/catalog': - specifier: ^2.9.0 - version: 2.9.0(@tiptap/pm@2.27.2) + specifier: ^2.9.1 + version: 2.9.1(@tiptap/pm@2.27.2) '@serve.zone/interfaces': specifier: ^5.3.0 version: 5.3.0 @@ -350,8 +350,8 @@ packages: '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} - '@design.estate/dees-catalog@3.49.1': - resolution: {integrity: sha512-YyaRu6uep5wiqx2wnQeeWXstNRkkEfTAH7uA9XiWwM+TwbWH83esu5PR8L+J4akz3VsSW26JlfRI+7GoWTs2mw==} + '@design.estate/dees-catalog@3.49.2': + resolution: {integrity: sha512-ChVf5IW/w1WSsfuI3BA1SX2QJFjZljnAvnyPDXnbzTXuOdTgs054p66JwlDca9KM8yBlndwibgAYJfD6/4sONw==} '@design.estate/dees-comms@1.0.30': resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} @@ -1141,8 +1141,8 @@ packages: '@push.rocks/smartdata@7.1.3': resolution: {integrity: sha512-7vQJ9pdRk450yn2m9tmGPdSRlQVmxFPZjHD4sGYsfqCQPg+GLFusu+H16zpf+jKzAq4F2ZBMPaYymJHXvXiVcw==} - '@push.rocks/smartdb@2.0.0': - resolution: {integrity: sha512-RGaXGOS+5c7Hru2XwoyavQuoZqrfIzUfF/AnnVA0GYOrj4P2S89fngp8QDczVyZq/IbkByYXz59foQmN/WDlWA==} + '@push.rocks/smartdb@2.1.1': + resolution: {integrity: sha512-bm+xYpuzSgS+EacNP3NppwNvpw9OZN3gmtVUgBdqyLLKYX0329bDN5X63V6vdrglFBV/+MKox43l8BQBwVdfjw==} '@push.rocks/smartdelay@3.0.5': resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} @@ -1583,8 +1583,8 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@serve.zone/catalog@2.9.0': - resolution: {integrity: sha512-7FgwS44pD/DFVj29jS0Kwwyn1i5h8cf4/yWMBEY8+8GO70ab3QctbcKMu+BVa1G3gIrpLqhpmxLFDoeL/zDnQA==} + '@serve.zone/catalog@2.9.1': + resolution: {integrity: sha512-W+4x5O834DiEtcqfVNFrP6qYlj/6c9UTR9oEl2Wthf0R2SN0zC6Hbs16US2kP+mmQBKAIDMQYvTUI9oaXqvcog==} '@serve.zone/interfaces@5.3.0': resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} @@ -4260,11 +4260,13 @@ packages: xterm-addon-fit@0.8.0: resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==} + deprecated: This package is now deprecated. Move to @xterm/addon-fit instead. peerDependencies: xterm: ^5.0.0 xterm@5.3.0: resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} + deprecated: This package is now deprecated. Move to @xterm/xterm instead. y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -4339,7 +4341,7 @@ snapshots: '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3) '@cloudflare/workers-types': 4.20260317.1 - '@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2) + '@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2) '@design.estate/dees-comms': 1.0.30 '@push.rocks/lik': 6.4.0 '@push.rocks/smartdelay': 3.0.5 @@ -4868,7 +4870,7 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 - '@design.estate/dees-catalog@3.49.1(@tiptap/pm@2.27.2)': + '@design.estate/dees-catalog@3.49.2(@tiptap/pm@2.27.2)': dependencies: '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 @@ -6127,9 +6129,19 @@ snapshots: - supports-color - vue - '@push.rocks/smartdb@2.0.0': + '@push.rocks/smartdb@2.1.1(@tiptap/pm@2.27.2)': dependencies: + '@api.global/typedserver': 8.4.6(@tiptap/pm@2.27.2) + '@design.estate/dees-element': 2.2.4 '@push.rocks/smartrust': 1.3.2 + transitivePeerDependencies: + - '@nuxt/kit' + - '@tiptap/pm' + - bufferutil + - react + - supports-color + - utf-8-validate + - vue '@push.rocks/smartdelay@3.0.5': dependencies: @@ -6894,10 +6906,10 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 - '@serve.zone/catalog@2.9.0(@tiptap/pm@2.27.2)': + '@serve.zone/catalog@2.9.1(@tiptap/pm@2.27.2)': dependencies: - '@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2) - '@design.estate/dees-domtools': 2.5.3 + '@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2) + '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 '@design.estate/dees-wcctools': 3.8.0 transitivePeerDependencies: diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8394051..03b4777 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '12.2.3', + version: '12.2.4', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 9456b4c..804fb91 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -2060,6 +2060,13 @@ export class DcRouter { const currentRoutes = this.constructorRoutes; this.remoteIngressManager.setRoutes(currentRoutes as any[]); + // Race-condition fix: if ConfigManagers finished before us, re-apply routes + // so the callback delivers the full merged set (including DB-stored routes) + // to our newly-created remoteIngressManager. + if (this.routeConfigManager) { + await this.routeConfigManager.applyRoutes(); + } + // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default) const riCfg = this.options.remoteIngressConfig; let tlsConfig: { certPem: string; keyPem: string } | undefined; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 8394051..03b4777 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '12.2.3', + version: '12.2.4', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 46c0182..3b40edc 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -427,6 +427,8 @@ export const setActiveViewAction = uiStatePart.createAction(async (state if (viewName === 'routes' && currentState.activeView !== 'routes') { setTimeout(() => { routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null); + // Also fetch profiles/targets for the Create Route dropdowns + profilesTargetsStatePart.dispatchAction(fetchProfilesAndTargetsAction, null); }, 100); } @@ -1413,6 +1415,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy export const createRouteAction = routeManagementStatePart.createAction<{ route: any; enabled?: boolean; + metadata?: any; }>(async (statePartArg, dataArg, actionContext): Promise => { const context = getActionContext(); const currentState = statePartArg.getState()!; @@ -1426,6 +1429,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{ identity: context.identity!, route: dataArg.route, enabled: dataArg.enabled, + metadata: dataArg.metadata, }); return await actionContext!.dispatch(fetchMergedRoutesAction, null); diff --git a/ts_web/elements/ops-view-routes.ts b/ts_web/elements/ops-view-routes.ts index 6e3513f..d50e035 100644 --- a/ts_web/elements/ops-view-routes.ts +++ b/ts_web/elements/ops-view-routes.ts @@ -24,6 +24,14 @@ export class OpsViewRoutes extends DeesElement { lastUpdated: 0, }; + @state() accessor profilesTargetsState: appstate.IProfilesTargetsState = { + profiles: [], + targets: [], + isLoading: false, + error: null, + lastUpdated: 0, + }; + constructor() { super(); const sub = appstate.routeManagementStatePart @@ -33,6 +41,13 @@ export class OpsViewRoutes extends DeesElement { }); this.rxSubscriptions.push(sub); + const ptSub = appstate.profilesTargetsStatePart + .select((s) => s) + .subscribe((ptState) => { + this.profilesTargetsState = ptState; + }); + this.rxSubscriptions.push(ptSub); + // Re-fetch routes when user logs in (fixes race condition where // the view is created before authentication completes) const loginSub = appstate.loginStatePart @@ -40,6 +55,7 @@ export class OpsViewRoutes extends DeesElement { .subscribe((isLoggedIn) => { if (isLoggedIn) { appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null); + appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null); } }); this.rxSubscriptions.push(loginSub); @@ -145,6 +161,7 @@ export class OpsViewRoutes extends DeesElement { enabled: mr.enabled, tags, id: mr.storedRouteId || mr.route.name || undefined, + metadata: mr.metadata, }; }); @@ -275,6 +292,7 @@ export class OpsViewRoutes extends DeesElement { }); } else { // Programmatic route + const meta = merged.metadata; await DeesModal.createAndShow({ heading: `Route: ${merged.route.name}`, content: html` @@ -282,6 +300,8 @@ export class OpsViewRoutes extends DeesElement {

Source: programmatic

Status: ${merged.enabled ? 'Enabled' : 'Disabled'}

ID: ${merged.storedRouteId}

+ ${meta?.securityProfileName ? html`

Security Profile: ${meta.securityProfileName}

` : ''} + ${meta?.networkTargetName ? html`

Network Target: ${meta.networkTargetName}

` : ''} `, menuOptions: [ @@ -319,6 +339,24 @@ export class OpsViewRoutes extends DeesElement { private async showCreateRouteDialog() { const { DeesModal } = await import('@design.estate/dees-catalog'); + const profiles = this.profilesTargetsState.profiles; + const targets = this.profilesTargetsState.targets; + + // Build dropdown options for profiles and targets + const profileOptions = [ + { key: '', option: '(none — inline security)' }, + ...profiles.map((p) => ({ + key: p.id, + option: `${p.name}${p.description ? ' — ' + p.description : ''}`, + })), + ]; + const targetOptions = [ + { key: '', option: '(none — inline target)' }, + ...targets.map((t) => ({ + key: t.id, + option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`, + })), + ]; await DeesModal.createAndShow({ heading: 'Add Programmatic Route', @@ -327,8 +365,10 @@ export class OpsViewRoutes extends DeesElement { - - + + + + `, menuOptions: [ @@ -362,15 +402,27 @@ export class OpsViewRoutes extends DeesElement { targets: [ { host: formData.targetHost || 'localhost', - port: parseInt(formData.targetPort, 10), + port: parseInt(formData.targetPort, 10) || 443, }, ], }, }; + // Build metadata if profile/target selected + const metadata: any = {}; + if (formData.securityProfileRef) { + metadata.securityProfileRef = formData.securityProfileRef; + } + if (formData.networkTargetRef) { + metadata.networkTargetRef = formData.networkTargetRef; + } + await appstate.routeManagementStatePart.dispatchAction( appstate.createRouteAction, - { route }, + { + route, + metadata: Object.keys(metadata).length > 0 ? metadata : undefined, + }, ); await modalArg.destroy(); }, diff --git a/ts_web/elements/ops-view-securityprofiles.ts b/ts_web/elements/ops-view-securityprofiles.ts index 126db64..f7f8b6d 100644 --- a/ts_web/elements/ops-view-securityprofiles.ts +++ b/ts_web/elements/ops-view-securityprofiles.ts @@ -131,8 +131,8 @@ export class OpsViewSecurityProfiles extends DeesElement { - - + + `, @@ -142,20 +142,16 @@ export class OpsViewSecurityProfiles extends DeesElement { action: async (modalArg: any) => { const form = modalArg.shadowRoot!.querySelector('dees-form'); const data = await form.collectFormData(); - const ipAllowList = data.ipAllowList - ? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean) - : undefined; - const ipBlockList = data.ipBlockList - ? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean) - : undefined; + const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : []; + const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : []; const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined; await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, { name: String(data.name), description: data.description ? String(data.description) : undefined, security: { - ...(ipAllowList ? { ipAllowList } : {}), - ...(ipBlockList ? { ipBlockList } : {}), + ...(ipAllowList.length > 0 ? { ipAllowList } : {}), + ...(ipBlockList.length > 0 ? { ipBlockList } : {}), ...(maxConnections ? { maxConnections } : {}), }, }); @@ -175,8 +171,8 @@ export class OpsViewSecurityProfiles extends DeesElement { - - + + `, @@ -186,12 +182,8 @@ export class OpsViewSecurityProfiles extends DeesElement { action: async (modalArg: any) => { const form = modalArg.shadowRoot!.querySelector('dees-form'); const data = await form.collectFormData(); - const ipAllowList = data.ipAllowList - ? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean) - : []; - const ipBlockList = data.ipBlockList - ? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean) - : []; + const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : []; + const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : []; const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined; await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {