2026-03-30 08:15:09 +00:00
import {
DeesElement ,
html ,
customElement ,
type TemplateResult ,
css ,
state ,
cssManager ,
} from '@design.estate/dees-element' ;
2026-03-30 16:49:58 +00:00
import * as plugins from '../plugins.js' ;
2026-03-30 08:15:09 +00:00
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' ;
declare global {
interface HTMLElementTagNameMap {
'ops-view-vpn' : OpsViewVpn ;
}
}
@customElement ( 'ops-view-vpn' )
export class OpsViewVpn extends DeesElement {
@state ( )
accessor vpnState : appstate.IVpnState = appstate . vpnStatePart . getState ( ) ! ;
constructor ( ) {
super ( ) ;
const sub = appstate . vpnStatePart . select ( ) . subscribe ( ( newState ) = > {
this . vpnState = newState ;
} ) ;
this . rxSubscriptions . push ( sub ) ;
}
async connectedCallback() {
await super . connectedCallback ( ) ;
await appstate . vpnStatePart . dispatchAction ( appstate . fetchVpnAction , null ) ;
}
public static styles = [
cssManager . defaultStyles ,
viewHostCss ,
css `
. vpnContainer {
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 . enabled {
background : $ { cssManager . bdTheme ( '#dcfce7' , '#14532d' ) } ;
color : $ { cssManager . bdTheme ( '#166534' , '#4ade80' ) } ;
}
. statusBadge . disabled {
background : $ { cssManager . bdTheme ( '#fef2f2' , '#450a0a' ) } ;
color : $ { cssManager . bdTheme ( '#991b1b' , '#f87171' ) } ;
}
. configDialog {
padding : 16px ;
background : $ { cssManager . bdTheme ( '#fffbeb' , '#1c1917' ) } ;
border : 1px solid $ { cssManager . bdTheme ( '#fbbf24' , '#92400e' ) } ;
border - radius : 8px ;
margin - bottom : 16px ;
}
. configDialog pre {
display : block ;
padding : 12px ;
background : $ { cssManager . bdTheme ( '#1f2937' , '#111827' ) } ;
color : # 10 b981 ;
border - radius : 4px ;
font - family : monospace ;
font - size : 12px ;
white - space : pre - wrap ;
word - break : break - all ;
margin : 8px 0 ;
user - select : all ;
max - height : 300px ;
overflow - y : auto ;
}
. configDialog . warning {
font - size : 12px ;
color : $ { cssManager . bdTheme ( '#92400e' , '#fbbf24' ) } ;
margin - top : 8px ;
}
. tagBadge {
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' ) } ;
margin - right : 4px ;
}
. serverInfo {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 200 px , 1 fr ) ) ;
gap : 12px ;
padding : 16px ;
background : $ { cssManager . bdTheme ( '#f9fafb' , '#111827' ) } ;
border - radius : 8px ;
border : 1px solid $ { cssManager . bdTheme ( '#e5e7eb' , '#1f2937' ) } ;
}
. serverInfo . infoItem {
display : flex ;
flex - direction : column ;
gap : 4px ;
}
. serverInfo . infoLabel {
font - size : 11px ;
font - weight : 600 ;
text - transform : uppercase ;
letter - spacing : 0.05em ;
color : $ { cssManager . bdTheme ( '#6b7280' , '#9ca3af' ) } ;
}
. serverInfo . infoValue {
font - size : 14px ;
font - family : monospace ;
color : $ { cssManager . bdTheme ( '#111827' , '#f9fafb' ) } ;
}
` ,
] ;
2026-03-31 09:53:37 +00:00
/** Look up connected client info by clientId */
private getConnectedInfo ( clientId : string ) : interfaces . data . IVpnConnectedClient | undefined {
return this . vpnState . connectedClients ? . find ( c = > c . clientId === clientId ) ;
}
2026-03-30 08:15:09 +00:00
render ( ) : TemplateResult {
const status = this . vpnState . status ;
const clients = this . vpnState . clients ;
2026-03-31 09:53:37 +00:00
const connectedClients = this . vpnState . connectedClients || [ ] ;
const connectedCount = connectedClients . length ;
2026-03-30 08:15:09 +00:00
const totalClients = clients . length ;
const enabledClients = clients . filter ( c = > c . enabled ) . length ;
const statsTiles : IStatsTile [ ] = [
{
id : 'totalClients' ,
title : 'Total Clients' ,
type : 'number' ,
value : totalClients ,
icon : 'lucide:users' ,
description : 'Registered VPN clients' ,
color : '#3b82f6' ,
} ,
{
id : 'connectedClients' ,
title : 'Connected' ,
type : 'number' ,
value : connectedCount ,
icon : 'lucide:link' ,
description : 'Currently connected' ,
color : '#10b981' ,
} ,
{
id : 'enabledClients' ,
title : 'Enabled' ,
type : 'number' ,
value : enabledClients ,
icon : 'lucide:shieldCheck' ,
description : 'Active client registrations' ,
color : '#8b5cf6' ,
} ,
{
id : 'serverStatus' ,
title : 'Server' ,
type : 'text' ,
value : status?.running ? 'Running' : 'Stopped' ,
icon : 'lucide:server' ,
2026-03-30 13:06:14 +00:00
description : status?.running ? 'Active' : 'VPN server not running' ,
2026-03-30 08:15:09 +00:00
color : status?.running ? '#10b981' : '#ef4444' ,
} ,
] ;
return html `
< ops - sectionheading > VPN < / o p s - s e c t i o n h e a d i n g >
2026-03-30 16:49:58 +00:00
< div class = "vpnContainer" >
2026-03-30 08:15:09 +00:00
$ { this . vpnState . newClientConfig ? html `
< div class = "configDialog" >
< strong > Client created successfully ! < / strong >
< div class = "warning" > Copy the WireGuard config now . It contains private keys that won ' t be shown again . < / div >
< pre > $ { this . vpnState . newClientConfig } < / pre >
< dees - button
@click = $ { async ( ) = > {
if ( navigator . clipboard && typeof navigator . clipboard . writeText === 'function' ) {
await navigator . clipboard . writeText ( this . vpnState . newClientConfig ! ) ;
}
const { DeesToast } = await import ( '@design.estate/dees-catalog' ) ;
DeesToast . createAndShow ( { message : 'Config copied to clipboard' , type : 'success' , duration : 3000 } ) ;
} }
> Copy to Clipboard < / d e e s - b u t t o n >
< dees - button
@click = $ { ( ) = > {
const blob = new Blob ( [ this . vpnState . newClientConfig ! ] , { type : 'text/plain' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = 'wireguard.conf' ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
} }
> Download . conf < / d e e s - b u t t o n >
2026-03-30 23:50:51 +00:00
< dees - button
@click = $ { async ( ) = > {
const dataUrl = await plugins . qrcode . toDataURL (
this . vpnState . newClientConfig ! ,
{ width : 400 , margin : 2 }
) ;
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
DeesModal . createAndShow ( {
heading : 'WireGuard QR Code' ,
content : html `
< div style = "text-align: center; padding: 16px;" >
< img src = "${dataUrl}" style = "max-width: 100%; image-rendering: pixelated;" / >
< p style = "margin-top: 12px; font-size: 13px; color: #9ca3af;" >
Scan with the WireGuard app on your phone
< / p >
< / div >
` ,
menuOptions : [
{ name : 'Close' , iconName : 'lucide:x' , action : async ( modalArg : any ) = > await modalArg . destroy ( ) } ,
] ,
} ) ;
} }
> Show QR Code < / d e e s - b u t t o n >
2026-03-30 08:15:09 +00:00
< dees - button
@click = $ { ( ) = > appstate . vpnStatePart . dispatchAction ( appstate . clearNewClientConfigAction , null ) }
> Dismiss < / d e e s - b u t t o n >
< / div >
` : ''}
2026-03-30 16:49:58 +00:00
< dees - statsgrid .tiles = $ { statsTiles } > < / d e e s - s t a t s g r i d >
2026-03-30 08:15:09 +00:00
$ { status ? html `
< div class = "serverInfo" >
< div class = "infoItem" >
< span class = "infoLabel" > Subnet < / span >
< span class = "infoValue" > $ { status . subnet } < / span >
< / div >
< div class = "infoItem" >
< span class = "infoLabel" > WireGuard Port < / span >
< span class = "infoValue" > $ { status . wgListenPort } < / span >
< / div >
$ { status . serverPublicKeys ? html `
< div class = "infoItem" >
< span class = "infoLabel" > WG Public Key < / span >
< span class = "infoValue" style = "font-size: 11px; word-break: break-all;" > $ { status . serverPublicKeys . wgPublicKey } < / span >
< / div >
` : ''}
< / div >
` : ''}
< dees - table
. heading1 = $ { 'VPN Clients' }
. heading2 = $ { 'Manage WireGuard and SmartVPN client registrations' }
. data = $ { clients }
2026-03-31 09:53:37 +00:00
. displayFunction = $ { ( client : interfaces.data.IVpnClient ) = > {
const conn = this . getConnectedInfo ( client . clientId ) ;
let statusHtml ;
if ( ! client . enabled ) {
statusHtml = html ` <span class="statusBadge disabled">disabled</span> ` ;
} else if ( conn ) {
const since = new Date ( conn . connectedSince ) . toLocaleString ( ) ;
statusHtml = html ` <span class="statusBadge enabled" title="Since ${ since } ">connected</span> ` ;
} else {
statusHtml = html ` <span class="statusBadge enabled" style="background: ${ cssManager . bdTheme ( '#eff6ff' , '#172554' ) } ; color: ${ cssManager . bdTheme ( '#1e40af' , '#60a5fa' ) } ;">offline</span> ` ;
}
return {
'Client ID' : client . clientId ,
'Status' : statusHtml ,
'VPN IP' : client . assignedIp || '-' ,
'Tags' : client . serverDefinedClientTags ? . length
? html ` ${ client . serverDefinedClientTags . map ( t = > html ` <span class="tagBadge"> ${ t } </span> ` ) } `
: '-' ,
'Description' : client . description || '-' ,
'Created' : new Date ( client . createdAt ) . toLocaleDateString ( ) ,
} ;
} }
2026-03-30 08:15:09 +00:00
. dataActions = $ { [
2026-03-30 16:49:58 +00:00
{
name : 'Create Client' ,
iconName : 'lucide:plus' ,
type : [ 'header' ] ,
actionFunc : async ( ) = > {
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
await DeesModal . createAndShow ( {
heading : 'Create VPN Client' ,
content : html `
< dees - form >
< dees - input - text .key = $ { 'clientId' } .label = $ { 'Client ID' } .required = $ { true } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'description' } .label = $ { 'Description' } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'tags' } .label = $ { 'Server-Defined Tags (comma-separated)' } > < / d e e s - i n p u t - t e x t >
< / d e e s - f o r m >
` ,
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 data = await form . collectFormData ( ) ;
if ( ! data . clientId ) return ;
const serverDefinedClientTags = data . tags
? data . tags . split ( ',' ) . map ( ( t : string ) = > t . trim ( ) ) . filter ( Boolean )
: undefined ;
await appstate . vpnStatePart . dispatchAction ( appstate . createVpnClientAction , {
clientId : data.clientId ,
description : data.description || undefined ,
serverDefinedClientTags ,
} ) ;
await modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
} ,
} ,
2026-03-30 08:15:09 +00:00
{
2026-03-31 09:53:37 +00:00
name : 'Detail' ,
iconName : 'lucide:info' ,
type : [ 'doubleClick' ] ,
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
const conn = this . getConnectedInfo ( client . clientId ) ;
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
// Fetch telemetry on-demand
let telemetryHtml = html ` <p style="color: #9ca3af;">Loading telemetry...</p> ` ;
try {
const request = new plugins . domtools . plugins . typedrequest . TypedRequest <
interfaces . requests . IReq_GetVpnClientTelemetry
> ( '/typedrequest' , 'getVpnClientTelemetry' ) ;
const response = await request . fire ( {
identity : appstate.loginStatePart.getState ( ) ! . identity ! ,
clientId : client.clientId ,
} ) ;
const t = response . telemetry ;
if ( t ) {
const formatBytes = ( b : number ) = > b > 1048576 ? ` ${ ( b / 1048576 ) . toFixed ( 1 ) } MB ` : b > 1024 ? ` ${ ( b / 1024 ) . toFixed ( 1 ) } KB ` : ` ${ b } B ` ;
telemetryHtml = html `
< div class = "serverInfo" style = "margin-top: 12px;" >
< div class = "infoItem" > < span class = "infoLabel" > Bytes Sent < / span > < span class = "infoValue" > $ { formatBytes ( t . bytesSent ) } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Bytes Received < / span > < span class = "infoValue" > $ { formatBytes ( t . bytesReceived ) } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Keepalives < / span > < span class = "infoValue" > $ { t . keepalivesReceived } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Last Keepalive < / span > < span class = "infoValue" > $ { t . lastKeepaliveAt ? new Date ( t . lastKeepaliveAt ) . toLocaleString ( ) : '-' } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Packets Dropped < / span > < span class = "infoValue" > $ { t . packetsDropped } < / span > < / div >
< / div >
` ;
} else {
telemetryHtml = html ` <p style="color: #9ca3af;">No telemetry available (client not connected)</p> ` ;
}
} catch {
telemetryHtml = html ` <p style="color: #9ca3af;">Telemetry unavailable</p> ` ;
}
DeesModal . createAndShow ( {
heading : ` Client: ${ client . clientId } ` ,
content : html `
< div class = "serverInfo" >
< div class = "infoItem" > < span class = "infoLabel" > Client ID < / span > < span class = "infoValue" > $ { client . clientId } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > VPN IP < / span > < span class = "infoValue" > $ { client . assignedIp || '-' } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Status < / span > < span class = "infoValue" > $ { ! client . enabled ? 'Disabled' : conn ? 'Connected' : 'Offline' } < / span > < / div >
$ { conn ? html `
< div class = "infoItem" > < span class = "infoLabel" > Connected Since < / span > < span class = "infoValue" > $ { new Date ( conn . connectedSince ) . toLocaleString ( ) } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Transport < / span > < span class = "infoValue" > $ { conn . transport } < / span > < / div >
` : ''}
< div class = "infoItem" > < span class = "infoLabel" > Description < / span > < span class = "infoValue" > $ { client . description || '-' } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Tags < / span > < span class = "infoValue" > $ { client . serverDefinedClientTags ? . join ( ', ' ) || '-' } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Created < / span > < span class = "infoValue" > $ { new Date ( client . createdAt ) . toLocaleString ( ) } < / span > < / div >
< div class = "infoItem" > < span class = "infoLabel" > Updated < / span > < span class = "infoValue" > $ { new Date ( client . updatedAt ) . toLocaleString ( ) } < / span > < / div >
< / div >
< h3 style = "margin: 16px 0 4px; font-size: 14px;" > Telemetry < / h3 >
$ { telemetryHtml }
` ,
menuOptions : [
{ name : 'Close' , iconName : 'lucide:x' , action : async ( m : any ) = > await m . destroy ( ) } ,
] ,
} ) ;
} ,
} ,
{
name : 'Enable' ,
iconName : 'lucide:power' ,
type : [ 'contextmenu' , 'inRow' ] ,
actionRelevancyCheckFunc : ( actionData : any ) = > ! actionData . item . enabled ,
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
await appstate . vpnStatePart . dispatchAction ( appstate . toggleVpnClientAction , {
clientId : client.clientId ,
enabled : true ,
} ) ;
} ,
} ,
{
name : 'Disable' ,
2026-03-30 08:15:09 +00:00
iconName : 'lucide:power' ,
2026-03-30 16:49:58 +00:00
type : [ 'contextmenu' , 'inRow' ] ,
2026-03-31 09:53:37 +00:00
actionRelevancyCheckFunc : ( actionData : any ) = > actionData . item . enabled ,
2026-03-30 17:08:57 +00:00
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
2026-03-30 08:15:09 +00:00
await appstate . vpnStatePart . dispatchAction ( appstate . toggleVpnClientAction , {
clientId : client.clientId ,
2026-03-31 09:53:37 +00:00
enabled : false ,
2026-03-30 08:15:09 +00:00
} ) ;
} ,
} ,
2026-03-30 16:49:58 +00:00
{
name : 'Export Config' ,
iconName : 'lucide:download' ,
type : [ 'contextmenu' , 'inRow' ] ,
2026-03-30 17:08:57 +00:00
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
const { DeesModal , DeesToast } = await import ( '@design.estate/dees-catalog' ) ;
const exportConfig = async ( format : 'wireguard' | 'smartvpn' ) = > {
try {
const request = new plugins . domtools . plugins . typedrequest . TypedRequest <
interfaces . requests . IReq_ExportVpnClientConfig
> ( '/typedrequest' , 'exportVpnClientConfig' ) ;
const response = await request . fire ( {
identity : appstate.loginStatePart.getState ( ) ! . identity ! ,
clientId : client.clientId ,
format ,
} ) ;
if ( response . success && response . config ) {
const ext = format === 'wireguard' ? 'conf' : 'json' ;
const blob = new Blob ( [ response . config ] , { type : 'text/plain' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = ` ${ client . clientId } . ${ ext } ` ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
DeesToast . createAndShow ( { message : ` ${ format } config downloaded ` , type : 'success' , duration : 3000 } ) ;
} else {
DeesToast . createAndShow ( { message : response.message || 'Export failed' , type : 'error' , duration : 5000 } ) ;
}
} catch ( err : any ) {
DeesToast . createAndShow ( { message : err.message || 'Export failed' , type : 'error' , duration : 5000 } ) ;
2026-03-30 16:49:58 +00:00
}
2026-03-30 17:08:57 +00:00
} ;
2026-03-30 23:50:51 +00:00
const showQrCode = async ( ) = > {
try {
const request = new plugins . domtools . plugins . typedrequest . TypedRequest <
interfaces . requests . IReq_ExportVpnClientConfig
> ( '/typedrequest' , 'exportVpnClientConfig' ) ;
const response = await request . fire ( {
identity : appstate.loginStatePart.getState ( ) ! . identity ! ,
clientId : client.clientId ,
format : 'wireguard' ,
} ) ;
if ( response . success && response . config ) {
const dataUrl = await plugins . qrcode . toDataURL (
response . config ,
{ width : 400 , margin : 2 }
) ;
DeesModal . createAndShow ( {
heading : ` QR Code: ${ client . clientId } ` ,
content : html `
< div style = "text-align: center; padding: 16px;" >
< img src = "${dataUrl}" style = "max-width: 100%; image-rendering: pixelated;" / >
< p style = "margin-top: 12px; font-size: 13px; color: #9ca3af;" >
Scan with the WireGuard app on your phone
< / p >
< / div >
` ,
menuOptions : [
{ name : 'Close' , iconName : 'lucide:x' , action : async ( m : any ) = > await m . destroy ( ) } ,
] ,
} ) ;
} else {
DeesToast . createAndShow ( { message : response.message || 'Export failed' , type : 'error' , duration : 5000 } ) ;
}
} catch ( err : any ) {
DeesToast . createAndShow ( { message : err.message || 'QR generation failed' , type : 'error' , duration : 5000 } ) ;
}
} ;
2026-03-30 17:08:57 +00:00
DeesModal . createAndShow ( {
heading : ` Export Config: ${ client . clientId } ` ,
content : html ` <p>Choose a config format to download.</p> ` ,
menuOptions : [
{
name : 'WireGuard (.conf)' ,
iconName : 'lucide:shield' ,
action : async ( modalArg : any ) = > {
await modalArg . destroy ( ) ;
await exportConfig ( 'wireguard' ) ;
} ,
} ,
{
name : 'SmartVPN (.json)' ,
iconName : 'lucide:braces' ,
action : async ( modalArg : any ) = > {
await modalArg . destroy ( ) ;
await exportConfig ( 'smartvpn' ) ;
} ,
} ,
2026-03-30 23:50:51 +00:00
{
name : 'QR Code (WireGuard)' ,
iconName : 'lucide:qr-code' ,
action : async ( modalArg : any ) = > {
await modalArg . destroy ( ) ;
await showQrCode ( ) ;
} ,
} ,
2026-03-30 17:08:57 +00:00
{
name : 'Cancel' ,
iconName : 'lucide:x' ,
action : async ( modalArg : any ) = > await modalArg . destroy ( ) ,
} ,
] ,
} ) ;
2026-03-30 16:49:58 +00:00
} ,
} ,
2026-03-31 09:53:37 +00:00
{
name : 'Edit' ,
iconName : 'lucide:pencil' ,
type : [ 'contextmenu' , 'inRow' ] ,
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
const currentDescription = client . description ? ? '' ;
const currentTags = client . serverDefinedClientTags ? . join ( ', ' ) ? ? '' ;
DeesModal . createAndShow ( {
heading : ` Edit: ${ client . clientId } ` ,
content : html `
< dees - form >
< dees - input - text .key = $ { 'description' } .label = $ { 'Description' } .value = $ { currentDescription } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'tags' } .label = $ { 'Server-Defined Tags (comma-separated)' } .value = $ { currentTags } > < / d e e s - i n p u t - t e x t >
< / 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 data = await form . collectFormData ( ) ;
const serverDefinedClientTags = data . tags
? data . tags . split ( ',' ) . map ( ( t : string ) = > t . trim ( ) ) . filter ( Boolean )
: [ ] ;
await appstate . vpnStatePart . dispatchAction ( appstate . updateVpnClientAction , {
clientId : client.clientId ,
description : data.description || undefined ,
serverDefinedClientTags ,
} ) ;
await modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
} ,
} ,
2026-03-30 16:49:58 +00:00
{
name : 'Rotate Keys' ,
iconName : 'lucide:rotate-cw' ,
type : [ 'contextmenu' ] ,
2026-03-30 17:08:57 +00:00
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
2026-03-30 16:49:58 +00:00
const { DeesModal , DeesToast } = await import ( '@design.estate/dees-catalog' ) ;
DeesModal . createAndShow ( {
heading : 'Rotate Client Keys' ,
content : html ` <p>Generate new keys for " ${ client . clientId } "? The old keys will be invalidated and the client will need the new config to reconnect.</p> ` ,
menuOptions : [
{ name : 'Cancel' , iconName : 'lucide:x' , action : async ( modalArg : any ) = > await modalArg . destroy ( ) } ,
{
name : 'Rotate' ,
iconName : 'lucide:rotate-cw' ,
action : async ( modalArg : any ) = > {
try {
const request = new plugins . domtools . plugins . typedrequest . TypedRequest <
interfaces . requests . IReq_RotateVpnClientKey
> ( '/typedrequest' , 'rotateVpnClientKey' ) ;
const response = await request . fire ( {
identity : appstate.loginStatePart.getState ( ) ! . identity ! ,
clientId : client.clientId ,
} ) ;
if ( response . success && response . wireguardConfig ) {
appstate . vpnStatePart . setState ( {
. . . appstate . vpnStatePart . getState ( ) ! ,
newClientConfig : response.wireguardConfig ,
} ) ;
}
await modalArg . destroy ( ) ;
} catch ( err : any ) {
DeesToast . createAndShow ( { message : err.message || 'Rotate failed' , type : 'error' , duration : 5000 } ) ;
}
} ,
} ,
] ,
} ) ;
} ,
} ,
2026-03-30 08:15:09 +00:00
{
name : 'Delete' ,
iconName : 'lucide:trash2' ,
2026-03-30 16:49:58 +00:00
type : [ 'contextmenu' ] ,
2026-03-30 17:08:57 +00:00
actionFunc : async ( actionData : any ) = > {
const client = actionData . item as interfaces . data . IVpnClient ;
2026-03-30 08:15:09 +00:00
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
DeesModal . createAndShow ( {
heading : 'Delete VPN Client' ,
content : html ` <p>Are you sure you want to delete client " ${ client . clientId } "?</p> ` ,
menuOptions : [
2026-03-30 16:49:58 +00:00
{ name : 'Cancel' , iconName : 'lucide:x' , action : async ( modalArg : any ) = > await modalArg . destroy ( ) } ,
2026-03-30 08:15:09 +00:00
{
name : 'Delete' ,
2026-03-30 16:49:58 +00:00
iconName : 'lucide:trash2' ,
action : async ( modalArg : any ) = > {
2026-03-30 08:15:09 +00:00
await appstate . vpnStatePart . dispatchAction ( appstate . deleteVpnClientAction , client . clientId ) ;
2026-03-30 16:49:58 +00:00
await modalArg . destroy ( ) ;
2026-03-30 08:15:09 +00:00
} ,
} ,
] ,
} ) ;
} ,
} ,
] }
> < / d e e s - t a b l e >
2026-03-30 16:49:58 +00:00
< / div >
2026-03-30 08:15:09 +00:00
` ;
}
}