Compare commits

...

12 Commits

Author SHA1 Message Date
947637eed7 v12.2.6
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 18:49:52 +00:00
5202c2ea27 fix(ops-ui): improve operations table actions and modal form handling for profiles and network targets 2026-04-02 18:49:52 +00:00
6684dc43da v12.2.5
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 17:59:51 +00:00
04ec387ce5 fix(dcrouter): sync allowed tunnel edges when merged routes change 2026-04-02 17:59:51 +00:00
f145798f39 v12.2.4
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 17:27:05 +00:00
55f5465a9a fix(routes): support profile and target metadata in route creation and refresh remote ingress routes after config initialization 2026-04-02 17:27:05 +00:00
0577f45ced v12.2.3
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 16:27:35 +00:00
7d23617f15 fix(repo): no changes to commit 2026-04-02 16:27:35 +00:00
02415f8c53 v12.2.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 16:26:24 +00:00
73a47e5a97 fix(route-config): sync applied routes to remote ingress manager after route updates 2026-04-02 16:26:24 +00:00
5e980812b0 v12.2.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 16:10:35 +00:00
76e9735cde fix(web-ui): align dees-table props and action handlers in security profile and network target views 2026-04-02 16:10:35 +00:00
12 changed files with 212 additions and 78 deletions

View File

@@ -1,5 +1,44 @@
# Changelog
## 2026-04-02 - 12.2.6 - fix(ops-ui)
improve operations table actions and modal form handling for profiles and network targets
- adds section headings for the Security Profiles and Network Targets views
- updates edit and delete actions to support in-row table actions in addition to context menus
- makes create and edit dialogs query forms safely from modal content and adds early returns when forms are unavailable
- enables the database configuration in the development watch server
## 2026-04-02 - 12.2.5 - fix(dcrouter)
sync allowed tunnel edges when merged routes change
- Triggers tunnelManager.syncAllowedEdges() after route updates are applied
- Keeps derived ports in the Rust hub binary aligned with merged route changes
## 2026-04-02 - 12.2.4 - fix(routes)
support profile and target metadata in route creation and refresh remote ingress routes after config initialization
- Re-applies routes to the remote ingress manager after config managers finish to avoid missing DB-backed routes during initialization
- Fetches profiles and targets when opening or authenticating into the routes view so route creation dropdowns are populated
- Includes selected security profile and network target metadata when creating programmatic routes and displays that metadata in route details
- Improves security profile forms by switching IP allow/block lists to list inputs instead of comma-separated text fields
- Updates UI dependencies including smartdb, dees-catalog, and serve.zone catalog
## 2026-04-02 - 12.2.3 - fix(repo)
no changes to commit
## 2026-04-02 - 12.2.2 - fix(route-config)
sync applied routes to remote ingress manager after route updates
- add an optional route-applied callback to RouteConfigManager
- forward merged SmartProxy routes to RemoteIngressManager whenever routes are updated
## 2026-04-02 - 12.2.1 - fix(web-ui)
align dees-table props and action handlers in security profile and network target views
- replace deprecated table heading prop with heading1 and heading2 in both admin views
- rename table action callbacks from action to actionFunc for create, refresh, edit, and delete actions
## 2026-04-02 - 12.2.0 - feat(config)
add reusable security profiles and network targets with route reference resolution

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "12.2.0",
"version": "12.2.6",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -35,14 +35,14 @@
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.49.1",
"@design.estate/dees-catalog": "^3.49.2",
"@design.estate/dees-element": "^2.2.4",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.3.1",
"@push.rocks/smartdata": "^7.1.3",
"@push.rocks/smartdb": "^2.0.0",
"@push.rocks/smartdb": "^2.1.1",
"@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartguard": "^3.1.0",
@@ -61,7 +61,7 @@
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartvpn": "1.19.1",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.9.0",
"@serve.zone/catalog": "^2.9.1",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.15.3",
"@tsclass/tsclass": "^9.5.0",

48
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0
version: 7.1.0
'@design.estate/dees-catalog':
specifier: ^3.49.1
version: 3.49.1(@tiptap/pm@2.27.2)
specifier: ^3.49.2
version: 3.49.2(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.2.4
version: 2.2.4
@@ -45,8 +45,8 @@ importers:
specifier: ^7.1.3
version: 7.1.3(socks@2.8.7)
'@push.rocks/smartdb':
specifier: ^2.0.0
version: 2.0.0
specifier: ^2.1.1
version: 2.1.1(@tiptap/pm@2.27.2)
'@push.rocks/smartdns':
specifier: ^7.9.0
version: 7.9.0
@@ -102,8 +102,8 @@ importers:
specifier: ^8.0.2
version: 8.0.2
'@serve.zone/catalog':
specifier: ^2.9.0
version: 2.9.0(@tiptap/pm@2.27.2)
specifier: ^2.9.1
version: 2.9.1(@tiptap/pm@2.27.2)
'@serve.zone/interfaces':
specifier: ^5.3.0
version: 5.3.0
@@ -350,8 +350,8 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.49.1':
resolution: {integrity: sha512-YyaRu6uep5wiqx2wnQeeWXstNRkkEfTAH7uA9XiWwM+TwbWH83esu5PR8L+J4akz3VsSW26JlfRI+7GoWTs2mw==}
'@design.estate/dees-catalog@3.49.2':
resolution: {integrity: sha512-ChVf5IW/w1WSsfuI3BA1SX2QJFjZljnAvnyPDXnbzTXuOdTgs054p66JwlDca9KM8yBlndwibgAYJfD6/4sONw==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -1141,8 +1141,8 @@ packages:
'@push.rocks/smartdata@7.1.3':
resolution: {integrity: sha512-7vQJ9pdRk450yn2m9tmGPdSRlQVmxFPZjHD4sGYsfqCQPg+GLFusu+H16zpf+jKzAq4F2ZBMPaYymJHXvXiVcw==}
'@push.rocks/smartdb@2.0.0':
resolution: {integrity: sha512-RGaXGOS+5c7Hru2XwoyavQuoZqrfIzUfF/AnnVA0GYOrj4P2S89fngp8QDczVyZq/IbkByYXz59foQmN/WDlWA==}
'@push.rocks/smartdb@2.1.1':
resolution: {integrity: sha512-bm+xYpuzSgS+EacNP3NppwNvpw9OZN3gmtVUgBdqyLLKYX0329bDN5X63V6vdrglFBV/+MKox43l8BQBwVdfjw==}
'@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
@@ -1583,8 +1583,8 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@serve.zone/catalog@2.9.0':
resolution: {integrity: sha512-7FgwS44pD/DFVj29jS0Kwwyn1i5h8cf4/yWMBEY8+8GO70ab3QctbcKMu+BVa1G3gIrpLqhpmxLFDoeL/zDnQA==}
'@serve.zone/catalog@2.9.1':
resolution: {integrity: sha512-W+4x5O834DiEtcqfVNFrP6qYlj/6c9UTR9oEl2Wthf0R2SN0zC6Hbs16US2kP+mmQBKAIDMQYvTUI9oaXqvcog==}
'@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
@@ -4260,11 +4260,13 @@ packages:
xterm-addon-fit@0.8.0:
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
peerDependencies:
xterm: ^5.0.0
xterm@5.3.0:
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
@@ -4339,7 +4341,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
'@cloudflare/workers-types': 4.20260317.1
'@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.4.0
'@push.rocks/smartdelay': 3.0.5
@@ -4868,7 +4870,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.49.1(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.49.2(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
@@ -6127,9 +6129,19 @@ snapshots:
- supports-color
- vue
'@push.rocks/smartdb@2.0.0':
'@push.rocks/smartdb@2.1.1(@tiptap/pm@2.27.2)':
dependencies:
'@api.global/typedserver': 8.4.6(@tiptap/pm@2.27.2)
'@design.estate/dees-element': 2.2.4
'@push.rocks/smartrust': 1.3.2
transitivePeerDependencies:
- '@nuxt/kit'
- '@tiptap/pm'
- bufferutil
- react
- supports-color
- utf-8-validate
- vue
'@push.rocks/smartdelay@3.0.5':
dependencies:
@@ -6894,10 +6906,10 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
'@serve.zone/catalog@2.9.0(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.9.1(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.3
'@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.0
transitivePeerDependencies:

View File

@@ -49,8 +49,7 @@ const devRouter = new DcRouter({
{ clientId: 'admin-desktop', serverDefinedClientTags: ['admin'], description: 'Admin workstation' },
],
},
// Disable db/mongo for dev
dbConfig: { enabled: false },
dbConfig: { enabled: true },
});
console.log('Starting DcRouter in development mode...');

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '12.2.0',
version: '12.2.6',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -478,6 +478,16 @@ export class DcRouter {
}
: undefined,
this.referenceResolver,
// Sync merged routes to RemoteIngressManager whenever routes change,
// then push updated derived ports to the Rust hub binary
(routes) => {
if (this.remoteIngressManager) {
this.remoteIngressManager.setRoutes(routes as any[]);
}
if (this.tunnelManager) {
this.tunnelManager.syncAllowedEdges();
}
},
);
this.apiTokenManager = new ApiTokenManager();
await this.apiTokenManager.initialize();
@@ -2054,6 +2064,13 @@ export class DcRouter {
const currentRoutes = this.constructorRoutes;
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
// Race-condition fix: if ConfigManagers finished before us, re-apply routes
// so the callback delivers the full merged set (including DB-stored routes)
// to our newly-created remoteIngressManager.
if (this.routeConfigManager) {
await this.routeConfigManager.applyRoutes();
}
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
const riCfg = this.options.remoteIngressConfig;
let tlsConfig: { certPem: string; keyPem: string } | undefined;

View File

@@ -23,6 +23,7 @@ export class RouteConfigManager {
private getHttp3Config?: () => IHttp3Config | undefined,
private getVpnAllowList?: (tags?: string[]) => string[],
private referenceResolver?: ReferenceResolver,
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
) {}
/** Expose stored routes map for reference resolution lookups. */
@@ -393,6 +394,12 @@ export class RouteConfigManager {
}
await smartProxy.updateRoutes(enabledRoutes);
// Notify listeners (e.g. RemoteIngressManager) of the merged route set
if (this.onRoutesApplied) {
this.onRoutesApplied(enabledRoutes);
}
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
}
}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '12.2.0',
version: '12.2.6',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -427,6 +427,8 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
if (viewName === 'routes' && currentState.activeView !== 'routes') {
setTimeout(() => {
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
// Also fetch profiles/targets for the Create Route dropdowns
profilesTargetsStatePart.dispatchAction(fetchProfilesAndTargetsAction, null);
}, 100);
}
@@ -1413,6 +1415,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
export const createRouteAction = routeManagementStatePart.createAction<{
route: any;
enabled?: boolean;
metadata?: any;
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
@@ -1426,6 +1429,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
identity: context.identity!,
route: dataArg.route,
enabled: dataArg.enabled,
metadata: dataArg.metadata,
});
return await actionContext!.dispatch(fetchMergedRoutesAction, null);

View File

@@ -64,10 +64,12 @@ export class OpsViewNetworkTargets extends DeesElement {
];
return html`
<ops-sectionheading>Network Targets</ops-sectionheading>
<div class="targetsContainer">
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
<dees-table
.heading=${'Network Targets'}
.heading1=${'Network Targets'}
.heading2=${'Reusable host:port destinations for routes'}
.data=${targets}
.displayFunction=${(target: interfaces.data.INetworkTarget) => ({
Name: target.name,
@@ -80,31 +82,33 @@ export class OpsViewNetworkTargets extends DeesElement {
name: 'Create Target',
iconName: 'lucide:plus',
type: ['header' as const],
action: async (_: any, table: any) => {
await this.showCreateTargetDialog(table);
actionFunc: async () => {
await this.showCreateTargetDialog();
},
},
{
name: 'Refresh',
iconName: 'lucide:rotateCw',
type: ['header' as const],
action: async () => {
actionFunc: async () => {
await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
},
},
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['contextmenu' as const],
action: async (target: interfaces.data.INetworkTarget, table: any) => {
await this.showEditTargetDialog(target, table);
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const target = actionData.item as interfaces.data.INetworkTarget;
await this.showEditTargetDialog(target);
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['contextmenu' as const],
action: async (target: interfaces.data.INetworkTarget) => {
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const target = actionData.item as interfaces.data.INetworkTarget;
await this.deleteTarget(target);
},
},
@@ -114,7 +118,7 @@ export class OpsViewNetworkTargets extends DeesElement {
`;
}
private async showCreateTargetDialog(table: any) {
private async showCreateTargetDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Create Network Target',
@@ -127,10 +131,12 @@ export class OpsViewNetworkTargets extends DeesElement {
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Create',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createTargetAction, {
@@ -142,12 +148,11 @@ export class OpsViewNetworkTargets extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}
private async showEditTargetDialog(target: interfaces.data.INetworkTarget, table: any) {
private async showEditTargetDialog(target: interfaces.data.INetworkTarget) {
const hostStr = Array.isArray(target.host) ? target.host.join(', ') : target.host;
const { DeesModal } = await import('@design.estate/dees-catalog');
@@ -162,10 +167,12 @@ export class OpsViewNetworkTargets extends DeesElement {
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Save',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateTargetAction, {
@@ -178,7 +185,6 @@ export class OpsViewNetworkTargets extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}

View File

@@ -24,6 +24,14 @@ export class OpsViewRoutes extends DeesElement {
lastUpdated: 0,
};
@state() accessor profilesTargetsState: appstate.IProfilesTargetsState = {
profiles: [],
targets: [],
isLoading: false,
error: null,
lastUpdated: 0,
};
constructor() {
super();
const sub = appstate.routeManagementStatePart
@@ -33,6 +41,13 @@ export class OpsViewRoutes extends DeesElement {
});
this.rxSubscriptions.push(sub);
const ptSub = appstate.profilesTargetsStatePart
.select((s) => s)
.subscribe((ptState) => {
this.profilesTargetsState = ptState;
});
this.rxSubscriptions.push(ptSub);
// Re-fetch routes when user logs in (fixes race condition where
// the view is created before authentication completes)
const loginSub = appstate.loginStatePart
@@ -40,6 +55,7 @@ export class OpsViewRoutes extends DeesElement {
.subscribe((isLoggedIn) => {
if (isLoggedIn) {
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
}
});
this.rxSubscriptions.push(loginSub);
@@ -145,6 +161,7 @@ export class OpsViewRoutes extends DeesElement {
enabled: mr.enabled,
tags,
id: mr.storedRouteId || mr.route.name || undefined,
metadata: mr.metadata,
};
});
@@ -275,6 +292,7 @@ export class OpsViewRoutes extends DeesElement {
});
} else {
// Programmatic route
const meta = merged.metadata;
await DeesModal.createAndShow({
heading: `Route: ${merged.route.name}`,
content: html`
@@ -282,6 +300,8 @@ export class OpsViewRoutes extends DeesElement {
<p>Source: <strong style="color: #0af;">programmatic</strong></p>
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
<p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
${meta?.securityProfileName ? html`<p>Security Profile: <strong style="color: #a78bfa;">${meta.securityProfileName}</strong></p>` : ''}
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
</div>
`,
menuOptions: [
@@ -319,6 +339,24 @@ export class OpsViewRoutes extends DeesElement {
private async showCreateRouteDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
const profiles = this.profilesTargetsState.profiles;
const targets = this.profilesTargetsState.targets;
// Build dropdown options for profiles and targets
const profileOptions = [
{ key: '', option: '(none — inline security)' },
...profiles.map((p) => ({
key: p.id,
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
})),
];
const targetOptions = [
{ key: '', option: '(none — inline target)' },
...targets.map((t) => ({
key: t.id,
option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`,
})),
];
await DeesModal.createAndShow({
heading: 'Add Programmatic Route',
@@ -327,8 +365,10 @@ export class OpsViewRoutes extends DeesElement {
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${''}></dees-input-dropdown>
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${''}></dees-input-dropdown>
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'}></dees-input-text>
</dees-form>
`,
menuOptions: [
@@ -362,15 +402,27 @@ export class OpsViewRoutes extends DeesElement {
targets: [
{
host: formData.targetHost || 'localhost',
port: parseInt(formData.targetPort, 10),
port: parseInt(formData.targetPort, 10) || 443,
},
],
},
};
// Build metadata if profile/target selected
const metadata: any = {};
if (formData.securityProfileRef) {
metadata.securityProfileRef = formData.securityProfileRef;
}
if (formData.networkTargetRef) {
metadata.networkTargetRef = formData.networkTargetRef;
}
await appstate.routeManagementStatePart.dispatchAction(
appstate.createRouteAction,
{ route },
{
route,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
},
);
await modalArg.destroy();
},

View File

@@ -64,10 +64,12 @@ export class OpsViewSecurityProfiles extends DeesElement {
];
return html`
<ops-sectionheading>Security Profiles</ops-sectionheading>
<div class="profilesContainer">
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
<dees-table
.heading=${'Security Profiles'}
.heading1=${'Security Profiles'}
.heading2=${'Reusable security configurations for routes'}
.data=${profiles}
.displayFunction=${(profile: interfaces.data.ISecurityProfile) => ({
Name: profile.name,
@@ -88,31 +90,33 @@ export class OpsViewSecurityProfiles extends DeesElement {
name: 'Create Profile',
iconName: 'lucide:plus',
type: ['header' as const],
action: async (_: any, table: any) => {
await this.showCreateProfileDialog(table);
actionFunc: async () => {
await this.showCreateProfileDialog();
},
},
{
name: 'Refresh',
iconName: 'lucide:rotateCw',
type: ['header' as const],
action: async () => {
actionFunc: async () => {
await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
},
},
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['contextmenu' as const],
action: async (profile: interfaces.data.ISecurityProfile, table: any) => {
await this.showEditProfileDialog(profile, table);
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const profile = actionData.item as interfaces.data.ISecurityProfile;
await this.showEditProfileDialog(profile);
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['contextmenu' as const],
action: async (profile: interfaces.data.ISecurityProfile) => {
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const profile = actionData.item as interfaces.data.ISecurityProfile;
await this.deleteProfile(profile);
},
},
@@ -122,7 +126,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
`;
}
private async showCreateProfileDialog(table: any) {
private async showCreateProfileDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Create Security Profile',
@@ -130,43 +134,40 @@ export class OpsViewSecurityProfiles extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'}></dees-input-text>
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'}></dees-input-text>
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Create',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
const ipAllowList = data.ipAllowList
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
: undefined;
const ipBlockList = data.ipBlockList
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
: undefined;
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
name: String(data.name),
description: data.description ? String(data.description) : undefined,
security: {
...(ipAllowList ? { ipAllowList } : {}),
...(ipBlockList ? { ipBlockList } : {}),
...(ipAllowList.length > 0 ? { ipAllowList } : {}),
...(ipBlockList.length > 0 ? { ipBlockList } : {}),
...(maxConnections ? { maxConnections } : {}),
},
});
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile, table: any) {
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile) {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: `Edit Profile: ${profile.name}`,
@@ -174,23 +175,21 @@ export class OpsViewSecurityProfiles extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'} .value=${(profile.security?.ipAllowList || []).join(', ')}></dees-input-text>
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'} .value=${(profile.security?.ipBlockList || []).join(', ')}></dees-input-text>
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipAllowList || []}></dees-input-list>
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipBlockList || []}></dees-input-list>
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Save',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
const ipAllowList = data.ipAllowList
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const ipBlockList = data.ipBlockList
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
@@ -206,7 +205,6 @@ export class OpsViewSecurityProfiles extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}