From b5e760ae07c0cbc44ca73472cabfaac8a8d8605a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 17 Feb 2026 11:56:54 +0000 Subject: [PATCH] feat(remote-ingress): Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI --- changelog.md | 9 +++ ts/00_commitinfo_data.ts | 2 +- ts_interfaces/data/remoteingress.ts | 2 + ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 30 ++++++- ts_web/elements/ops-view-remoteingress.ts | 99 ++++++++++++++++------- 6 files changed, 114 insertions(+), 30 deletions(-) diff --git a/changelog.md b/changelog.md index e050ecb..c84f298 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-17 - 6.7.0 - feat(remote-ingress) +Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI + +- Add effectiveListenPorts?: number[] to IRemoteIngress interface (present in API responses) +- Make createRemoteIngressAction.listenPorts optional and update creation modal to allow empty ports (auto-derived) +- Add toggleRemoteIngressAction to enable/disable remote ingress edges and wire up Enable/Disable row/context-menu actions +- Update getPortsHtml to prefer manual listenPorts, fall back to effectiveListenPorts, show '(auto)' when derived and 'none' when no ports +- Standardize UI actions to use inRow/contextmenu and actionFunc signatures; update create modal to use explicit Cancel/Create menu options and collect form data programmatically + ## 2026-02-17 - 6.6.1 - fix(icons) standardize icon identifiers to lucide-prefixed names across operational views diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 1928b36..dba153b 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: '6.6.1', + version: '6.7.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_interfaces/data/remoteingress.ts b/ts_interfaces/data/remoteingress.ts index 93b8b32..3e27e0f 100644 --- a/ts_interfaces/data/remoteingress.ts +++ b/ts_interfaces/data/remoteingress.ts @@ -12,6 +12,8 @@ export interface IRemoteIngress { tags?: string[]; createdAt: number; updatedAt: number; + /** Effective ports derived from route configs — only present in API responses. */ + effectiveListenPorts?: number[]; } /** diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 1928b36..dba153b 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: '6.6.1', + version: '6.7.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 278ced2..916370f 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -821,7 +821,7 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn export const createRemoteIngressAction = remoteIngressStatePart.createAction<{ name: string; - listenPorts: number[]; + listenPorts?: number[]; tags?: string[]; }>(async (statePartArg, dataArg) => { const context = getActionContext(); @@ -924,6 +924,34 @@ export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction( } ); +export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{ + id: string; + enabled: boolean; +}>(async (statePartArg, dataArg) => { + const context = getActionContext(); + const currentState = statePartArg.getState(); + + try { + const request = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_UpdateRemoteIngress + >('/typedrequest', 'updateRemoteIngress'); + + await request.fire({ + identity: context.identity, + id: dataArg.id, + enabled: dataArg.enabled, + }); + + await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); + return statePartArg.getState(); + } catch (error) { + return { + ...currentState, + error: error instanceof Error ? error.message : 'Failed to toggle edge', + }; + } +}); + // Combined refresh action for efficient polling async function dispatchCombinedRefreshAction() { const context = getActionContext(); diff --git a/ts_web/elements/ops-view-remoteingress.ts b/ts_web/elements/ops-view-remoteingress.ts index 14db9b0..522b710 100644 --- a/ts_web/elements/ops-view-remoteingress.ts +++ b/ts_web/elements/ops-view-remoteingress.ts @@ -187,7 +187,7 @@ export class OpsViewRemoteIngress extends DeesElement { name: edge.name, status: this.getEdgeStatusHtml(edge), publicIp: this.getEdgePublicIp(edge.id), - ports: this.getPortsHtml(edge.listenPorts), + ports: this.getPortsHtml(edge), tunnels: this.getEdgeTunnelCount(edge.id), lastHeartbeat: this.getLastHeartbeat(edge.id), })} @@ -198,42 +198,80 @@ export class OpsViewRemoteIngress extends DeesElement { type: ['header'], actionFunc: async () => { const { DeesModal } = await import('@design.estate/dees-catalog'); - const result = await DeesModal.createAndShow({ + const modal = await DeesModal.createAndShow({ heading: 'Create Edge Node', content: html` - + `, - menuOptions: [], - }); - if (result) { - const formData = result as any; - const ports = (formData.name ? formData.listenPorts : '443') - .split(',') - .map((p: string) => parseInt(p.trim(), 10)) - .filter((p: number) => !isNaN(p)); - const tags = formData.tags - ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) - : undefined; - await appstate.remoteIngressStatePart.dispatchAction( - appstate.createRemoteIngressAction, + menuOptions: [ { - name: formData.name, - listenPorts: ports, - tags, + name: 'Cancel', + iconName: 'lucide:x', + action: async (modalArg: any) => await modalArg.destroy(), }, - ); - } + { + name: 'Create', + iconName: 'lucide:plus', + action: async (modalArg: any) => { + const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); + if (!form) return; + const formData = await form.collectFormData(); + const name = formData.name; + if (!name) return; + const portsStr = formData.listenPorts?.trim(); + const listenPorts = portsStr + ? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p)) + : undefined; + const tags = formData.tags + ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) + : undefined; + await appstate.remoteIngressStatePart.dispatchAction( + appstate.createRemoteIngressAction, + { name, listenPorts, tags }, + ); + await modalArg.destroy(); + }, + }, + ], + }); + }, + }, + { + name: 'Enable', + iconName: 'lucide:play', + type: ['inRow', 'contextmenu'] as any, + actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled, + actionFunc: async (actionData: any) => { + const edge = actionData.item as interfaces.data.IRemoteIngress; + await appstate.remoteIngressStatePart.dispatchAction( + appstate.toggleRemoteIngressAction, + { id: edge.id, enabled: true }, + ); + }, + }, + { + name: 'Disable', + iconName: 'lucide:pause', + type: ['inRow', 'contextmenu'] as any, + actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled, + actionFunc: async (actionData: any) => { + const edge = actionData.item as interfaces.data.IRemoteIngress; + await appstate.remoteIngressStatePart.dispatchAction( + appstate.toggleRemoteIngressAction, + { id: edge.id, enabled: false }, + ); }, }, { name: 'Regenerate Secret', iconName: 'lucide:key', - type: ['row'], - action: async (edge: interfaces.data.IRemoteIngress) => { + type: ['inRow', 'contextmenu'] as any, + actionFunc: async (actionData: any) => { + const edge = actionData.item as interfaces.data.IRemoteIngress; await appstate.remoteIngressStatePart.dispatchAction( appstate.regenerateRemoteIngressSecretAction, edge.id, @@ -243,8 +281,9 @@ export class OpsViewRemoteIngress extends DeesElement { { name: 'Delete', iconName: 'lucide:trash2', - type: ['row'], - action: async (edge: interfaces.data.IRemoteIngress) => { + type: ['inRow', 'contextmenu'] as any, + actionFunc: async (actionData: any) => { + const edge = actionData.item as interfaces.data.IRemoteIngress; await appstate.remoteIngressStatePart.dispatchAction( appstate.deleteRemoteIngressAction, edge.id, @@ -277,8 +316,14 @@ export class OpsViewRemoteIngress extends DeesElement { return status?.publicIp || '-'; } - private getPortsHtml(ports: number[]): TemplateResult { - return html`
${ports.map(p => html`${p}`)}
`; + private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult { + const hasManualPorts = edge.listenPorts && edge.listenPorts.length > 0; + const ports = hasManualPorts ? edge.listenPorts : (edge.effectiveListenPorts || []); + const isAuto = !hasManualPorts && ports.length > 0; + if (ports.length === 0) { + return html`none`; + } + return html`
${ports.map(p => html`${p}`)}${isAuto ? html`(auto)` : ''}
`; } private getEdgeTunnelCount(edgeId: string): number {