2026-02-16 11:25:16 +00:00
import {
DeesElement ,
html ,
customElement ,
type TemplateResult ,
css ,
state ,
cssManager ,
} from '@design.estate/dees-element' ;
2026-04-08 08:24:55 +00:00
import * as appstate from '../../appstate.js' ;
import * as interfaces from '../../../dist_ts_interfaces/index.js' ;
import { viewHostCss } from '../shared/css.js' ;
2026-02-16 11:25:16 +00:00
import { type IStatsTile } from '@design.estate/dees-catalog' ;
declare global {
interface HTMLElementTagNameMap {
'ops-view-remoteingress' : OpsViewRemoteIngress ;
}
}
@customElement ( 'ops-view-remoteingress' )
export class OpsViewRemoteIngress extends DeesElement {
@state ( )
2026-03-26 07:40:56 +00:00
accessor riState : appstate.IRemoteIngressState = appstate . remoteIngressStatePart . getState ( ) ! ;
2026-02-16 11:25:16 +00:00
constructor ( ) {
super ( ) ;
2026-03-27 18:46:11 +00:00
const sub = appstate . remoteIngressStatePart . select ( ) . subscribe ( ( newState ) = > {
2026-02-16 11:25:16 +00:00
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 10 px ;
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 12 px ;
background : $ { cssManager . bdTheme ( '#1f2937' , '#111827' ) } ;
color : # 10 b981 ;
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 8 px ;
border - radius : 4px ;
font - size : 12px ;
font - weight : 500 ;
background : $ { cssManager . bdTheme ( '#eff6ff' , '#172554' ) } ;
color : $ { cssManager . bdTheme ( '#1e40af' , '#60a5fa' ) } ;
}
2026-02-17 14:17:18 +00:00
. 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' ) } ;
}
2026-02-16 11:25:16 +00:00
` ,
] ;
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 `
2026-04-08 11:08:18 +00:00
< dees - heading level = "3" > Remote Ingress < / d e e s - h e a d i n g >
2026-02-16 11:25:16 +00:00
2026-02-18 18:47:18 +00:00
$ { this . riState . newEdgeId ? html `
2026-02-16 11:25:16 +00:00
< div class = "secretDialog" >
2026-02-18 18:47:18 +00:00
< strong > Edge created successfully ! < / strong >
< div class = "warning" > Copy the connection token now . Use it with edge . start ( { token : '...' } ) . < / div >
2026-02-16 11:25:16 +00:00
< dees - button
2026-02-18 18:47:18 +00:00
@click = $ { async ( ) = > {
const { DeesToast } = await import ( '@design.estate/dees-catalog' ) ;
try {
2026-03-26 07:40:56 +00:00
const response = await appstate . fetchConnectionToken ( this . riState . newEdgeId ! ) ;
2026-02-18 18:47:18 +00:00
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 } ) ;
}
2026-03-26 07:40:56 +00:00
} catch ( err : unknown ) {
DeesToast . show ( { message : ` Failed: ${ ( err as Error ) . message } ` , type : 'error' , duration : 4000 } ) ;
2026-02-18 18:47:18 +00:00
}
} }
> Copy Connection Token < / d e e s - b u t t o n >
< dees - button
@click = $ { ( ) = > appstate . remoteIngressStatePart . dispatchAction ( appstate . clearNewEdgeIdAction , null ) }
2026-02-16 11:25:16 +00:00
> Dismiss < / d e e s - b u t t o n >
< / div >
` : ''}
< div class = "remoteIngressContainer" >
< dees - statsgrid .tiles = $ { statsTiles } > < / d e e s - s t a t s g r i d >
< dees - table
. heading1 = $ { 'Edge Nodes' }
. heading2 = $ { 'Manage remote ingress edge registrations' }
. data = $ { this . riState . edges }
2026-04-08 15:26:12 +00:00
. rowKey = $ { 'id' }
. highlightUpdates = $ { 'flash' }
2026-04-08 07:11:21 +00:00
. showColumnFilters = $ { true }
2026-02-16 11:25:16 +00:00
. displayFunction = $ { ( edge : interfaces.data.IRemoteIngress ) = > ( {
name : edge.name ,
status : this.getEdgeStatusHtml ( edge ) ,
publicIp : this.getEdgePublicIp ( edge . id ) ,
2026-02-17 11:56:54 +00:00
ports : this.getPortsHtml ( edge ) ,
2026-02-16 11:25:16 +00:00
tunnels : this.getEdgeTunnelCount ( edge . id ) ,
lastHeartbeat : this.getLastHeartbeat ( edge . id ) ,
} ) }
. dataActions = $ { [
2026-02-16 22:42:30 +00:00
{
name : 'Create Edge Node' ,
iconName : 'lucide:plus' ,
type : [ 'header' ] ,
actionFunc : async ( ) = > {
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
2026-02-17 11:56:54 +00:00
const modal = await DeesModal . createAndShow ( {
2026-02-16 22:42:30 +00:00
heading : 'Create Edge Node' ,
content : html `
< dees - form >
< dees - input - text .key = $ { 'name' } .label = $ { 'Name' } .required = $ { true } > < / d e e s - i n p u t - t e x t >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'listenPorts' } .label = $ { 'Manual Ports' } .description = $ { 'Comma-separated port numbers, optional' } > < / d e e s - i n p u t - t e x t >
2026-02-17 14:17:18 +00:00
< dees - input - checkbox .key = $ { 'autoDerivePorts' } .label = $ { 'Auto-derive ports from routes' } .value = $ { true } > < / d e e s - i n p u t - c h e c k b o x >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'tags' } .label = $ { 'Tags' } .description = $ { 'Comma-separated, optional' } > < / d e e s - i n p u t - t e x t >
2026-02-16 22:42:30 +00:00
< / d e e s - f o r m >
` ,
2026-02-17 11:56:54 +00:00
menuOptions : [
{
name : 'Cancel' ,
iconName : 'lucide:x' ,
action : async ( modalArg : any ) = > await modalArg . destroy ( ) ,
} ,
2026-02-16 22:42:30 +00:00
{
2026-02-17 11:56:54 +00:00
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 ;
2026-02-17 14:17:18 +00:00
const autoDerivePorts = formData . autoDerivePorts !== false ;
2026-02-17 11:56:54 +00:00
const tags = formData . tags
? formData . tags . split ( ',' ) . map ( ( t : string ) = > t . trim ( ) ) . filter ( Boolean )
: undefined ;
await appstate . remoteIngressStatePart . dispatchAction (
appstate . createRemoteIngressAction ,
2026-02-17 14:17:18 +00:00
{ name , listenPorts , autoDerivePorts , tags } ,
2026-02-17 11:56:54 +00:00
) ;
await modalArg . destroy ( ) ;
} ,
2026-02-16 22:42:30 +00:00
} ,
2026-02-17 11:56:54 +00:00
] ,
} ) ;
} ,
} ,
{
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 } ,
) ;
2026-02-16 22:42:30 +00:00
} ,
} ,
2026-02-17 14:17:18 +00:00
{
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 `
< dees - form >
< dees - input - text .key = $ { 'name' } .label = $ { 'Name' } .value = $ { edge.name } > < / d e e s - i n p u t - t e x t >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'listenPorts' } .label = $ { 'Manual Ports' } .description = $ { 'Comma-separated port numbers' } .value = $ { ( edge.listenPorts | | [ ] ) .join ( ', ' ) } > < / d e e s - i n p u t - t e x t >
2026-02-17 14:17:18 +00:00
< dees - input - checkbox .key = $ { 'autoDerivePorts' } .label = $ { 'Auto-derive ports from routes' } .value = $ { edge.autoDerivePorts ! = = false } > < / d e e s - i n p u t - c h e c k b o x >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'tags' } .label = $ { 'Tags' } .description = $ { 'Comma-separated' } .value = $ { ( edge.tags | | [ ] ) .join ( ', ' ) } > < / d e e s - i n p u t - t e x t >
2026-02-17 14:17:18 +00:00
< / d e e s - f o r m >
` ,
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 ;
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 ,
tags ,
} ,
) ;
await modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
} ,
} ,
2026-02-16 11:25:16 +00:00
{
name : 'Regenerate Secret' ,
iconName : 'lucide:key' ,
2026-02-17 11:56:54 +00:00
type : [ 'inRow' , 'contextmenu' ] as any ,
actionFunc : async ( actionData : any ) = > {
const edge = actionData . item as interfaces . data . IRemoteIngress ;
2026-02-16 11:25:16 +00:00
await appstate . remoteIngressStatePart . dispatchAction (
appstate . regenerateRemoteIngressSecretAction ,
edge . id ,
) ;
} ,
} ,
2026-02-18 06:05:46 +00:00
{
name : 'Copy Token' ,
2026-02-18 18:47:18 +00:00
iconName : 'lucide:ClipboardCopy' ,
2026-02-18 06:05:46 +00:00
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 ) {
2026-02-18 18:47:18 +00:00
// 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 ) ;
}
2026-02-18 06:05:46 +00:00
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 } ) ;
}
2026-03-26 07:40:56 +00:00
} catch ( err : unknown ) {
DeesToast . show ( { message : ` Failed: ${ ( err as Error ) . message } ` , type : 'error' , duration : 4000 } ) ;
2026-02-18 06:05:46 +00:00
}
} ,
} ,
2026-02-16 11:25:16 +00:00
{
name : 'Delete' ,
iconName : 'lucide:trash2' ,
2026-02-17 11:56:54 +00:00
type : [ 'inRow' , 'contextmenu' ] as any ,
actionFunc : async ( actionData : any ) = > {
const edge = actionData . item as interfaces . data . IRemoteIngress ;
2026-02-16 11:25:16 +00:00
await appstate . remoteIngressStatePart . dispatchAction (
appstate . deleteRemoteIngressAction ,
edge . id ,
) ;
} ,
} ,
] }
> < / d e e s - t a b l e >
< / div >
` ;
}
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 ` <span class="statusBadge disabled">Disabled</span> ` ;
}
const status = this . getEdgeStatus ( edge . id ) ;
if ( status ? . connected ) {
return html ` <span class="statusBadge connected">Connected</span> ` ;
}
return html ` <span class="statusBadge disconnected">Disconnected</span> ` ;
}
private getEdgePublicIp ( edgeId : string ) : string {
const status = this . getEdgeStatus ( edgeId ) ;
return status ? . publicIp || '-' ;
}
2026-02-17 11:56:54 +00:00
private getPortsHtml ( edge : interfaces.data.IRemoteIngress ) : TemplateResult {
2026-02-17 14:17:18 +00:00
const manualPorts = edge . manualPorts || [ ] ;
const derivedPorts = edge . derivedPorts || [ ] ;
if ( manualPorts . length === 0 && derivedPorts . length === 0 ) {
2026-02-17 11:56:54 +00:00
return html ` <span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span> ` ;
}
2026-02-17 14:17:18 +00:00
return html ` <div class="portsDisplay"> ${ manualPorts . map ( p = > html ` <span class="portBadge manual"> ${ p } </span> ` ) } ${ derivedPorts . map ( p = > html ` <span class="portBadge derived"> ${ p } </span> ` ) } ${ derivedPorts . length > 0 ? html ` <span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span> ` : '' } </div> ` ;
2026-02-16 11:25:16 +00:00
}
private getEdgeTunnelCount ( edgeId : string ) : number {
const status = this . getEdgeStatus ( edgeId ) ;
return status ? . activeTunnels || 0 ;
}
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 ` ;
}
}