import { DeesElement, html, customElement, type TemplateResult, css, state, cssManager, } from '@design.estate/dees-element'; import * as appstate from '../../appstate.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; import { viewHostCss } from '../shared/css.js'; import { type IStatsTile } from '@design.estate/dees-catalog'; const performanceProfileOptions = [ { key: '', option: 'Default' }, { key: 'balanced', option: 'Balanced' }, { key: 'throughput', option: 'Throughput' }, { key: 'highConcurrency', option: 'High concurrency' }, ]; function getDropdownKey(value: any): string { return typeof value === 'string' ? value : value?.key || ''; } declare global { interface HTMLElementTagNameMap { 'ops-view-remoteingress': OpsViewRemoteIngress; } } @customElement('ops-view-remoteingress') export class OpsViewRemoteIngress extends DeesElement { @state() accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState()!; constructor() { super(); const sub = appstate.remoteIngressStatePart.select().subscribe((newState) => { this.riState = newState; }); this.rxSubscriptions.push(sub); } async connectedCallback() { await super.connectedCallback(); await appstate.remoteIngressStatePart.dispatchAction(appstate.fetchRemoteIngressAction, null); } public static styles = [ cssManager.defaultStyles, viewHostCss, css` .remoteIngressContainer { display: flex; flex-direction: column; gap: 24px; } .statusBadge { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; } .statusBadge.connected { background: ${cssManager.bdTheme('#dcfce7', '#14532d')}; color: ${cssManager.bdTheme('#166534', '#4ade80')}; } .statusBadge.disconnected { background: ${cssManager.bdTheme('#fef2f2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#f87171')}; } .statusBadge.disabled { background: ${cssManager.bdTheme('#f3f4f6', '#374151')}; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .secretDialog { padding: 16px; background: ${cssManager.bdTheme('#fffbeb', '#1c1917')}; border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')}; border-radius: 8px; margin-bottom: 16px; } .secretDialog code { display: block; padding: 8px 12px; background: ${cssManager.bdTheme('#1f2937', '#111827')}; color: #10b981; border-radius: 4px; font-family: monospace; font-size: 13px; word-break: break-all; margin: 8px 0; user-select: all; } .secretDialog .warning { font-size: 12px; color: ${cssManager.bdTheme('#92400e', '#fbbf24')}; margin-top: 8px; } .portsDisplay { display: flex; gap: 4px; flex-wrap: wrap; } .portBadge { display: inline-flex; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')}; } .portBadge.manual { background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')}; } .portBadge.derived { background: ${cssManager.bdTheme('#ecfdf5', '#022c22')}; color: ${cssManager.bdTheme('#047857', '#34d399')}; border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')}; } .metricStack { display: flex; flex-direction: column; gap: 2px; font-size: 12px; line-height: 1.35; } .metricMuted { color: var(--text-muted, #6b7280); } .settingsNote { margin: 12px 0 0; font-size: 12px; line-height: 1.5; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } `, ]; render(): TemplateResult { const totalEdges = this.riState.edges.length; const connectedEdges = this.riState.statuses.filter(s => s.connected).length; const disconnectedEdges = totalEdges - connectedEdges; const activeTunnels = this.riState.statuses.reduce((sum, s) => sum + s.activeTunnels, 0); const statsTiles: IStatsTile[] = [ { id: 'totalEdges', title: 'Total Edges', type: 'number', value: totalEdges, icon: 'lucide:server', description: 'Registered edge nodes', color: '#3b82f6', }, { id: 'connectedEdges', title: 'Connected', type: 'number', value: connectedEdges, icon: 'lucide:link', description: 'Currently connected edges', color: '#10b981', }, { id: 'disconnectedEdges', title: 'Disconnected', type: 'number', value: disconnectedEdges, icon: 'lucide:unlink', description: 'Offline edge nodes', color: disconnectedEdges > 0 ? '#ef4444' : '#6b7280', }, { id: 'activeTunnels', title: 'Active Tunnels', type: 'number', value: activeTunnels, icon: 'lucide:cable', description: 'Active client connections', color: '#8b5cf6', }, ]; return html` Remote Ingress ${this.riState.newEdgeId ? html`
Edge created successfully!
Copy the connection token now. Use it with edge.start({ token: '...' }).
{ const { DeesToast } = await import('@design.estate/dees-catalog'); try { const response = await appstate.fetchConnectionToken(this.riState.newEdgeId!); if (response.success && response.token) { if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { await navigator.clipboard.writeText(response.token); } else { const textarea = document.createElement('textarea'); textarea.value = response.token; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } DeesToast.show({ message: 'Connection token copied!', type: 'success', duration: 3000 }); } else { DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 }); } } catch (err: unknown) { DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 }); } }} >Copy Connection Token appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeIdAction, null)} >Dismiss
` : ''}
({ name: edge.name, status: this.getEdgeStatusHtml(edge), transport: this.getTransportHtml(edge.id), publicIp: this.getEdgePublicIp(edge.id), ports: this.getPortsHtml(edge), smtpEgress: this.getEgressHtml(edge), tunnels: this.getEdgeTunnelCount(edge.id), maxConnections: this.getMaxConnectionsHtml(edge), window: this.getWindowHtml(edge.id), queues: this.getQueuesHtml(edge.id), traffic: this.getTrafficHtml(edge.id), lastHeartbeat: this.getLastHeartbeat(edge.id), })} .dataActions=${[ { name: 'Create Edge Node', iconName: 'lucide:plus', type: ['header'], actionFunc: async () => { const { DeesModal } = await import('@design.estate/dees-catalog'); const modal = await DeesModal.createAndShow({ heading: 'Create Edge Node', content: html` `, menuOptions: [ { 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 autoDerivePorts = formData.autoDerivePorts !== false; let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined; let egress: interfaces.data.IRemoteIngressEgressPolicyConfig; try { performance = this.collectPerformanceOverride(formData); egress = this.collectEgressPolicy(formData); } catch (err: unknown) { const { DeesToast } = await import('@design.estate/dees-catalog'); DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 }); return; } const tags = formData.tags ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined; await appstate.remoteIngressStatePart.dispatchAction( appstate.createRemoteIngressAction, { name, listenPorts, autoDerivePorts, performance, egress, tags }, ); await modalArg.destroy(); }, }, ], }); }, }, { name: 'Hub Settings', iconName: 'lucide:slidersHorizontal', type: ['header' as const], actionFunc: async () => { await this.showHubSettingsDialog(); }, }, { name: 'Enable', iconName: 'lucide:play', type: ['inRow', 'contextmenu'] as any, actionRelevancyCheckFunc: (endpoint: any) => !endpoint.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: (endpoint: any) => endpoint.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: 'Edit', iconName: 'lucide:pencil', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const edge = actionData.item as interfaces.data.IRemoteIngress; const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: `Edit Edge: ${edge.name}`, content: html` `, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, { name: 'Save', iconName: 'lucide:check', action: async (modalArg: any) => { const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); if (!form) return; const formData = await form.collectFormData(); const portsStr = formData.listenPorts?.trim(); const listenPorts = portsStr ? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p)) : []; const autoDerivePorts = formData.autoDerivePorts !== false; let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined; let egress: interfaces.data.IRemoteIngressEgressPolicyConfig; try { performance = this.collectPerformanceOverride(formData, edge.performance); egress = this.collectEgressPolicy(formData, edge.egress); } catch (err: unknown) { const { DeesToast } = await import('@design.estate/dees-catalog'); DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 }); return; } const tags = formData.tags ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : []; await appstate.remoteIngressStatePart.dispatchAction( appstate.updateRemoteIngressAction, { id: edge.id, name: formData.name || edge.name, listenPorts, autoDerivePorts, performance, egress, tags, }, ); await modalArg.destroy(); }, }, ], }); }, }, { name: 'Regenerate Secret', iconName: 'lucide:key', 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, ); }, }, { name: 'Copy Token', iconName: 'lucide:ClipboardCopy', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const edge = actionData.item as interfaces.data.IRemoteIngress; const { DeesToast } = await import('@design.estate/dees-catalog'); try { const response = await appstate.fetchConnectionToken(edge.id); if (response.success && response.token) { // Use clipboard API with fallback for non-HTTPS contexts if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { await navigator.clipboard.writeText(response.token); } else { const textarea = document.createElement('textarea'); textarea.value = response.token; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } DeesToast.show({ message: `Connection token copied for ${edge.name}`, type: 'success', duration: 3000 }); } else { DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 }); } } catch (err: unknown) { DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 }); } }, }, { name: 'Delete', iconName: 'lucide:trash2', 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, ); }, }, ]} >
`; } private getEdgeStatus(edgeId: string): interfaces.data.IRemoteIngressStatus | undefined { return this.riState.statuses.find(s => s.edgeId === edgeId); } private getEdgeStatusHtml(edge: interfaces.data.IRemoteIngress): TemplateResult { if (!edge.enabled) { return html`Disabled`; } const status = this.getEdgeStatus(edge.id); if (status?.connected) { return html`Connected`; } return html`Disconnected`; } private getEdgePublicIp(edgeId: string): string { const status = this.getEdgeStatus(edgeId); return status?.publicIp || '-'; } private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult { const manualPorts = edge.manualPorts || []; const derivedPorts = edge.derivedPorts || []; if (manualPorts.length === 0 && derivedPorts.length === 0) { return html`none`; } return html`
${manualPorts.map(p => html`${p}`)}${derivedPorts.map(p => html`${p}`)}${derivedPorts.length > 0 ? html`(auto)` : ''}
`; } private getEdgeTunnelCount(edgeId: string): number { const status = this.getEdgeStatus(edgeId); return status?.activeTunnels || 0; } private getMaxConnectionsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult | string { const status = this.getEdgeStatus(edge.id); const override = edge.performance?.maxStreamsPerEdge; const effective = status?.performance?.maxStreamsPerEdge; if (!override && !effective) return '-'; return html`
${override || effective} ${override ? 'edge override' : 'hub default'}
`; } private getEgressHtml(edge: interfaces.data.IRemoteIngress): TemplateResult | string { if (!edge.egress?.enabled) return '-'; return html`
SMTP port ${(edge.egress.allowedPorts || [25]).join(', ')}
`; } private getTransportHtml(edgeId: string): TemplateResult | string { const status = this.getEdgeStatus(edgeId); if (!status?.connected) return '-'; const mode = status.transportMode || 'unknown'; const label = mode === 'quic' ? 'QUIC' : mode === 'tcpTls' ? 'TCP/TLS' : mode; return html`
${label}${status.fallbackUsed ? 'fallback' : status.performance?.profile || 'default'}
`; } private getWindowHtml(edgeId: string): TemplateResult | string { const status = this.getEdgeStatus(edgeId); if (!status?.connected || !status.flowControl) return '-'; if (!status.flowControl.applies) { return html`
native QUICmax ${status.performance?.maxStreamsPerEdge || '-'} streams
`; } return html`
${this.formatBytes(status.flowControl.currentWindowBytes)} window ${this.formatBytes(status.flowControl.estimatedInFlightBytes)} est. in-flight
`; } private getQueuesHtml(edgeId: string): TemplateResult | string { const status = this.getEdgeStatus(edgeId); if (!status?.connected || !status.queues) return '-'; return html`
C ${status.queues.ctrlQueueDepth} / D ${status.queues.dataQueueDepth}S ${status.queues.sustainedQueueDepth}
`; } private getTrafficHtml(edgeId: string): TemplateResult | string { const status = this.getEdgeStatus(edgeId); if (!status?.connected || !status.traffic) return '-'; const drops = (status.traffic.rejectedStreams || 0) + (status.udp?.droppedDatagrams || 0); return html`
${this.formatBytes(status.traffic.bytesIn)} in / ${this.formatBytes(status.traffic.bytesOut)} out ${drops} rejected/dropped
`; } private getLastHeartbeat(edgeId: string): string { const status = this.getEdgeStatus(edgeId); if (!status?.lastHeartbeat) return '-'; const ago = Date.now() - status.lastHeartbeat; if (ago < 60000) return `${Math.floor(ago / 1000)}s ago`; if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`; return `${Math.floor(ago / 3600000)}h ago`; } private formatBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value = value / 1024; unitIndex++; } return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`; } private collectPerformanceOverride( formData: Record, base?: interfaces.data.IRemoteIngressPerformanceConfig, ): interfaces.data.IRemoteIngressPerformanceConfig | undefined { const next: interfaces.data.IRemoteIngressPerformanceConfig = { ...(base || {}) }; const maxStreamsText = `${formData.maxStreamsPerEdge || ''}`.trim(); if (maxStreamsText) { const maxStreamsPerEdge = Number.parseInt(maxStreamsText, 10); if (!Number.isInteger(maxStreamsPerEdge) || maxStreamsPerEdge < 1) { throw new Error('Max Connections must be a positive integer'); } next.maxStreamsPerEdge = maxStreamsPerEdge; } else { delete next.maxStreamsPerEdge; } if (Object.keys(next).length > 0) { return next; } return base ? {} : undefined; } private collectEgressPolicy( formData: Record, base?: interfaces.data.IRemoteIngressEgressPolicyConfig, ): interfaces.data.IRemoteIngressEgressPolicyConfig { const enabled = formData.egressEnabled !== false && Boolean(formData.egressEnabled); if (!enabled) { return { enabled: false }; } const next: interfaces.data.IRemoteIngressEgressPolicyConfig = { ...(base || {}), enabled: true, allowedPorts: [25], }; const hostPatternsText = `${formData.egressAllowedHostPatterns || ''}`.trim(); if (hostPatternsText) { next.allowedHostPatterns = hostPatternsText.split(',').map((pattern) => pattern.trim()).filter(Boolean); } else { delete next.allowedHostPatterns; } const maxStreamsText = `${formData.egressMaxConcurrentStreams || ''}`.trim(); if (maxStreamsText) { const maxConcurrentStreams = Number.parseInt(maxStreamsText, 10); if (!Number.isInteger(maxConcurrentStreams) || maxConcurrentStreams < 1) { throw new Error('SMTP Egress Max Streams must be a positive integer'); } next.maxConcurrentStreams = maxConcurrentStreams; } else { delete next.maxConcurrentStreams; } return next; } private async showHubSettingsDialog(): Promise { const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); const hubSettings = this.riState.hubSettings; const performance = hubSettings?.performance || {}; const selectedProfile = performanceProfileOptions.find((option) => option.key === (performance.profile || '')) || performanceProfileOptions[0]; const updatedAt = hubSettings?.updatedAt ? new Date(hubSettings.updatedAt).toLocaleString() : 'not persisted yet'; await DeesModal.createAndShow({ heading: 'RemoteIngress Hub Settings', content: html`

Saving applies DB-backed hub settings. Enabling or disabling the hub restarts SmartProxy so tunneled traffic and route metadata stay consistent. Last updated: ${updatedAt} by ${hubSettings?.updatedBy || 'default'}.

`, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, { name: 'Save', iconName: 'lucide:check', action: async (modalArg: any) => { const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); if (!form) return; const formData = await form.collectFormData(); let performanceSettings: interfaces.data.IRemoteIngressPerformanceConfig | null; let tunnelPort: number; try { tunnelPort = this.parseRequiredPort(formData.tunnelPort, 'Tunnel Port'); performanceSettings = this.collectHubPerformanceSettings(formData, performance); } catch (err: unknown) { DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 }); return; } const nextState = await appstate.remoteIngressStatePart.dispatchAction( appstate.updateRemoteIngressHubSettingsAction, { enabled: formData.enabled !== false, tunnelPort, hubDomain: `${formData.hubDomain || ''}`.trim() || null, performance: performanceSettings, }, ); if (nextState.error) { DeesToast.show({ message: nextState.error, type: 'error', duration: 4000 }); return; } await modalArg.destroy(); DeesToast.show({ message: 'RemoteIngress hub settings saved', type: 'success', duration: 3000 }); }, }, ], }); } private collectHubPerformanceSettings( formData: Record, currentPerformance: interfaces.data.IRemoteIngressPerformanceConfig, ): interfaces.data.IRemoteIngressPerformanceConfig | null { const next: interfaces.data.IRemoteIngressPerformanceConfig = { ...currentPerformance }; const profile = getDropdownKey(formData.profile) as interfaces.data.TRemoteIngressPerformanceProfile | ''; if (profile) { next.profile = profile; } else { delete next.profile; } this.assignOptionalPositiveIntegerSetting(next, 'maxStreamsPerEdge', formData.maxStreamsPerEdge, 'Max Connections / Edge'); this.assignOptionalPositiveIntegerSetting(next, 'clientWriteTimeoutMs', formData.clientWriteTimeoutMs, 'Client Write Timeout'); this.assignOptionalPositiveIntegerSetting(next, 'firstDataConnectTimeoutMs', formData.firstDataConnectTimeoutMs, 'First Data Timeout'); const serverFirstPortsText = `${formData.serverFirstPorts || ''}`.trim(); if (serverFirstPortsText) { const serverFirstPorts = this.parsePortList(serverFirstPortsText, 'Server-first Ports'); if (serverFirstPorts.includes(443)) { throw new Error('Port 443 is client-first TLS and must not be listed as server-first'); } next.serverFirstPorts = serverFirstPorts; } else { delete next.serverFirstPorts; } return Object.keys(next).length > 0 ? next : null; } private assignOptionalPositiveIntegerSetting( target: interfaces.data.IRemoteIngressPerformanceConfig, key: 'maxStreamsPerEdge' | 'clientWriteTimeoutMs' | 'firstDataConnectTimeoutMs', value: any, label: string, ): void { const text = `${value || ''}`.trim(); if (!text) { delete target[key]; return; } const parsed = Number.parseInt(text, 10); if (!Number.isInteger(parsed) || parsed < 1) { throw new Error(`${label} must be a positive integer`); } target[key] = parsed; } private parsePortList(value: any, label: string): number[] { const text = `${value || ''}`.trim(); if (!text) { return []; } const ports = text.split(',').map((part) => Number.parseInt(part.trim(), 10)); for (const port of ports) { if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`${label} must contain valid port numbers`); } } return [...new Set(ports)].sort((a, b) => a - b); } private parseRequiredPort(value: any, label: string): number { const port = Number.parseInt(`${value || ''}`.trim(), 10); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`${label} must be a valid port number`); } return port; } }