diff --git a/changelog.md b/changelog.md
index d4fcd45..94ba157 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,12 @@
# Changelog
+## 2026-03-30 - 11.17.0 - feat(vpn)
+expand VPN operations view with client management and config export actions
+
+- adds predefined VPN clients to the dev server configuration for local testing
+- adds table actions to create clients, export WireGuard configs, rotate client keys, toggle access, and delete clients
+- updates the VPN view layout and stats grid binding to match the current component API
+
## 2026-03-30 - 11.16.0 - feat(vpn)
add destination-based VPN routing policy and standardize socket proxy forwarding
diff --git a/test_watch/devserver.ts b/test_watch/devserver.ts
index de296ef..f4d0cfa 100644
--- a/test_watch/devserver.ts
+++ b/test_watch/devserver.ts
@@ -25,6 +25,16 @@ const devRouter = new DcRouter({
},
],
},
+ // VPN with pre-defined clients
+ vpnConfig: {
+ enabled: true,
+ serverEndpoint: 'vpn.dev.local',
+ clients: [
+ { clientId: 'dev-laptop', serverDefinedClientTags: ['engineering', 'dev'], description: 'Developer laptop' },
+ { clientId: 'ci-runner', serverDefinedClientTags: ['engineering', 'ci'], description: 'CI/CD pipeline' },
+ { clientId: 'admin-desktop', serverDefinedClientTags: ['admin'], description: 'Admin workstation' },
+ ],
+ },
// Disable cache/mongo for dev
cacheConfig: { enabled: false },
});
diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts
index 8dae047..11d2044 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: '11.16.0',
+ version: '11.17.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts
index 8dae047..11d2044 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: '11.16.0',
+ version: '11.17.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
diff --git a/ts_web/elements/ops-view-vpn.ts b/ts_web/elements/ops-view-vpn.ts
index 144dbc7..ea95ef7 100644
--- a/ts_web/elements/ops-view-vpn.ts
+++ b/ts_web/elements/ops-view-vpn.ts
@@ -7,6 +7,7 @@ import {
state,
cssManager,
} from '@design.estate/dees-element';
+import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { viewHostCss } from './shared/css.js';
@@ -188,6 +189,7 @@ export class OpsViewVpn extends DeesElement {
return html`
Generate new keys for "${client.clientId}"? The old keys will be invalidated and the client will need the new config to reconnect.
`, + 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 }); + } + }, + }, + ], + }); + }, + }, { name: 'Delete', iconName: 'lucide:trash2', - action: async (client: interfaces.data.IVpnClient) => { + type: ['contextmenu'], + actionFunc: async (client: interfaces.data.IVpnClient) => { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Delete VPN Client', content: html`Are you sure you want to delete client "${client.clientId}"?
`, menuOptions: [ - { name: 'Cancel', action: async (modal: any) => modal.destroy() }, + { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() }, { name: 'Delete', - action: async (modal: any) => { + iconName: 'lucide:trash2', + action: async (modalArg: any) => { await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId); - modal.destroy(); + await modalArg.destroy(); }, }, ], @@ -290,37 +410,8 @@ export class OpsViewVpn extends DeesElement { }, }, ]} - .createNewItem=${async () => { - const { DeesModal, DeesForm, DeesInputText } = await import('@design.estate/dees-catalog'); - DeesModal.createAndShow({ - heading: 'Create VPN Client', - content: html` -