Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 529a4bae00 | |||
| 49606ae007 | |||
| 31a6510d8b | |||
| b5e760ae07 | |||
| ea32babaac | |||
| a4ddedaf46 |
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.8.0 - feat(remote-ingress)
|
||||||
|
support auto-deriving ports for remote ingress edges and expose manual/derived port breakdown in API and UI
|
||||||
|
|
||||||
|
- Add autoDerivePorts flag to IRemoteIngress with default true and migration to set existing stored edges to autoDerivePorts = true
|
||||||
|
- RemoteIngressManager: getEffectiveListenPorts now returns the union of manual + derived ports when autoDerivePorts is enabled; added getPortBreakdown to return manual vs derived lists
|
||||||
|
- API handlers updated: create/update requests accept autoDerivePorts; responses now include effectiveListenPorts, manualPorts, and derivedPorts (secrets still masked)
|
||||||
|
- Web UI updated: create and edit dialogs include an Auto-derive checkbox; port badges now visually distinguish manual vs derived ports; added updateRemoteIngressAction
|
||||||
|
- Non-breaking change: new field defaults to true so existing behavior is preserved
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.7.0 - feat(remote-ingress)
|
||||||
|
Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI
|
||||||
|
|
||||||
|
- Add effectiveListenPorts?: number[] to IRemoteIngress interface (present in API responses)
|
||||||
|
- Make createRemoteIngressAction.listenPorts optional and update creation modal to allow empty ports (auto-derived)
|
||||||
|
- Add toggleRemoteIngressAction to enable/disable remote ingress edges and wire up Enable/Disable row/context-menu actions
|
||||||
|
- Update getPortsHtml to prefer manual listenPorts, fall back to effectiveListenPorts, show '(auto)' when derived and 'none' when no ports
|
||||||
|
- Standardize UI actions to use inRow/contextmenu and actionFunc signatures; update create modal to use explicit Cancel/Create menu options and collect form data programmatically
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.6.1 - fix(icons)
|
||||||
|
standardize icon identifiers to lucide-prefixed names across operational views
|
||||||
|
|
||||||
|
- Replaced legacy/ambiguous icon names with 'lucide:...' identifiers in four UI modules: ts_web/elements/ops-view-certificates.ts, ops-view-network.ts, ops-view-overview.ts, and ops-view-security.ts.
|
||||||
|
- Updated common action/menu icons (e.g. arrowsRotate -> lucide:RefreshCw, magnifyingGlass -> lucide:Search, copy -> lucide:Copy, fileExport -> lucide:FileOutput).
|
||||||
|
- Mapped dashboard/tile icons to lucide equivalents (e.g. server -> lucide:Server, networkWired/sitemap -> lucide:Network, download/upload -> lucide:Download/Upload, microchip/memory -> lucide:Cpu/MemoryStick).
|
||||||
|
- Normalized alert and status icons to lucide names (e.g. triangleExclamation -> lucide:TriangleAlert, shield/userShield -> lucide:Shield/ShieldCheck, clock/clockRotateLeft -> lucide:Clock/History).
|
||||||
|
|
||||||
## 2026-02-17 - 6.6.0 - feat(remoteingress)
|
## 2026-02-17 - 6.6.0 - feat(remoteingress)
|
||||||
derive effective remote ingress listen ports from route configs and expose them via ops API
|
derive effective remote ingress listen ports from route configs and expose them via ops API
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "6.6.0",
|
"version": "6.8.0",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.6.0',
|
version: '6.8.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,17 @@ export class RemoteIngressHandler {
|
|||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { edges: [] };
|
return { edges: [] };
|
||||||
}
|
}
|
||||||
// Return edges without secrets, enriched with effective listen ports
|
// Return edges without secrets, enriched with effective listen ports and breakdown
|
||||||
const edges = manager.getAllEdges().map((e) => ({
|
const edges = manager.getAllEdges().map((e) => {
|
||||||
...e,
|
const breakdown = manager.getPortBreakdown(e);
|
||||||
secret: '********', // Never expose secrets via API
|
return {
|
||||||
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
...e,
|
||||||
}));
|
secret: '********', // Never expose secrets via API
|
||||||
|
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
||||||
|
manualPorts: breakdown.manual,
|
||||||
|
derivedPorts: breakdown.derived,
|
||||||
|
};
|
||||||
|
});
|
||||||
return { edges };
|
return { edges };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -50,6 +55,7 @@ export class RemoteIngressHandler {
|
|||||||
dataArg.name,
|
dataArg.name,
|
||||||
dataArg.listenPorts || [],
|
dataArg.listenPorts || [],
|
||||||
dataArg.tags,
|
dataArg.tags,
|
||||||
|
dataArg.autoDerivePorts ?? true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync allowed edges with the hub
|
// Sync allowed edges with the hub
|
||||||
@@ -102,6 +108,7 @@ export class RemoteIngressHandler {
|
|||||||
const edge = await manager.updateEdge(dataArg.id, {
|
const edge = await manager.updateEdge(dataArg.id, {
|
||||||
name: dataArg.name,
|
name: dataArg.name,
|
||||||
listenPorts: dataArg.listenPorts,
|
listenPorts: dataArg.listenPorts,
|
||||||
|
autoDerivePorts: dataArg.autoDerivePorts,
|
||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
tags: dataArg.tags,
|
tags: dataArg.tags,
|
||||||
});
|
});
|
||||||
@@ -115,7 +122,17 @@ export class RemoteIngressHandler {
|
|||||||
await tunnelManager.syncAllowedEdges();
|
await tunnelManager.syncAllowedEdges();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, edge: { ...edge, secret: '********' } };
|
const breakdown = manager.getPortBreakdown(edge);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
edge: {
|
||||||
|
...edge,
|
||||||
|
secret: '********',
|
||||||
|
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
||||||
|
manualPorts: breakdown.manual,
|
||||||
|
derivedPorts: breakdown.derived,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export class RemoteIngressManager {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
||||||
if (edge) {
|
if (edge) {
|
||||||
|
// Migration: old edges without autoDerivePorts default to true
|
||||||
|
if ((edge as any).autoDerivePorts === undefined) {
|
||||||
|
edge.autoDerivePorts = true;
|
||||||
|
await this.storageManager.setJSON(key, edge);
|
||||||
|
}
|
||||||
this.edges.set(edge.id, edge);
|
this.edges.set(edge.id, edge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,13 +96,28 @@ export class RemoteIngressManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the effective listen ports for an edge.
|
* Get the effective listen ports for an edge.
|
||||||
* Returns manual listenPorts if non-empty, otherwise derives ports from tagged routes.
|
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
|
||||||
*/
|
*/
|
||||||
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
|
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
|
||||||
if (edge.listenPorts && edge.listenPorts.length > 0) {
|
const manualPorts = edge.listenPorts || [];
|
||||||
return edge.listenPorts;
|
const shouldDerive = edge.autoDerivePorts !== false;
|
||||||
}
|
if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
|
||||||
return this.derivePortsForEdge(edge.id, edge.tags);
|
const derivedPorts = this.derivePortsForEdge(edge.id, edge.tags);
|
||||||
|
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get manual and derived port breakdown for an edge (used in API responses).
|
||||||
|
* Derived ports exclude any ports already present in the manual list.
|
||||||
|
*/
|
||||||
|
public getPortBreakdown(edge: IRemoteIngress): { manual: number[]; derived: number[] } {
|
||||||
|
const manual = edge.listenPorts || [];
|
||||||
|
const shouldDerive = edge.autoDerivePorts !== false;
|
||||||
|
if (!shouldDerive) return { manual, derived: [] };
|
||||||
|
const manualSet = new Set(manual);
|
||||||
|
const allDerived = this.derivePortsForEdge(edge.id, edge.tags);
|
||||||
|
const derived = allDerived.filter((p) => !manualSet.has(p));
|
||||||
|
return { manual, derived };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +127,7 @@ export class RemoteIngressManager {
|
|||||||
name: string,
|
name: string,
|
||||||
listenPorts: number[] = [],
|
listenPorts: number[] = [],
|
||||||
tags?: string[],
|
tags?: string[],
|
||||||
|
autoDerivePorts: boolean = true,
|
||||||
): Promise<IRemoteIngress> {
|
): Promise<IRemoteIngress> {
|
||||||
const id = plugins.uuid.v4();
|
const id = plugins.uuid.v4();
|
||||||
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||||
@@ -118,6 +139,7 @@ export class RemoteIngressManager {
|
|||||||
secret,
|
secret,
|
||||||
listenPorts,
|
listenPorts,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
autoDerivePorts,
|
||||||
tags: tags || [],
|
tags: tags || [],
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -150,6 +172,7 @@ export class RemoteIngressManager {
|
|||||||
updates: {
|
updates: {
|
||||||
name?: string;
|
name?: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
},
|
},
|
||||||
@@ -161,6 +184,7 @@ export class RemoteIngressManager {
|
|||||||
|
|
||||||
if (updates.name !== undefined) edge.name = updates.name;
|
if (updates.name !== undefined) edge.name = updates.name;
|
||||||
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
||||||
|
if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
|
||||||
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
||||||
if (updates.tags !== undefined) edge.tags = updates.tags;
|
if (updates.tags !== undefined) edge.tags = updates.tags;
|
||||||
edge.updatedAt = Date.now();
|
edge.updatedAt = Date.now();
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ export interface IRemoteIngress {
|
|||||||
secret: string;
|
secret: string;
|
||||||
listenPorts: number[];
|
listenPorts: number[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||||
|
autoDerivePorts: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
/** Effective ports (union of manual + derived) — only present in API responses. */
|
||||||
|
effectiveListenPorts?: number[];
|
||||||
|
/** Ports explicitly set by the user — only present in API responses. */
|
||||||
|
manualPorts?: number[];
|
||||||
|
/** Ports auto-derived from route configs — only present in API responses. */
|
||||||
|
derivedPorts?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
identity?: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
name: string;
|
name: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -57,6 +58,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.6.0',
|
version: '6.8.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -821,7 +821,8 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
|||||||
|
|
||||||
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
name: string;
|
name: string;
|
||||||
listenPorts: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -836,6 +837,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
name: dataArg.name,
|
name: dataArg.name,
|
||||||
listenPorts: dataArg.listenPorts,
|
listenPorts: dataArg.listenPorts,
|
||||||
|
autoDerivePorts: dataArg.autoDerivePorts,
|
||||||
tags: dataArg.tags,
|
tags: dataArg.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -883,6 +885,40 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateRemoteIngress
|
||||||
|
>('/typedrequest', 'updateRemoteIngress');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
name: dataArg.name,
|
||||||
|
listenPorts: dataArg.listenPorts,
|
||||||
|
autoDerivePorts: dataArg.autoDerivePorts,
|
||||||
|
tags: dataArg.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update edge',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||||
async (statePartArg, edgeId) => {
|
async (statePartArg, edgeId) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -924,6 +960,34 @@ export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateRemoteIngress
|
||||||
|
>('/typedrequest', 'updateRemoteIngress');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to toggle edge',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Combined refresh action for efficient polling
|
// Combined refresh action for efficient polling
|
||||||
async function dispatchCombinedRefreshAction() {
|
async function dispatchCombinedRefreshAction() {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Total Certificates',
|
title: 'Total Certificates',
|
||||||
value: summary.total,
|
value: summary.total,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'shieldHalved',
|
icon: 'lucide:ShieldHalf',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -183,7 +183,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Valid',
|
title: 'Valid',
|
||||||
value: summary.valid,
|
value: summary.valid,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'check',
|
icon: 'lucide:Check',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -191,7 +191,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Expiring Soon',
|
title: 'Expiring Soon',
|
||||||
value: summary.expiring,
|
value: summary.expiring,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'clock',
|
icon: 'lucide:Clock',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -199,7 +199,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Failed / Expired',
|
title: 'Failed / Expired',
|
||||||
value: summary.failed + summary.expired,
|
value: summary.failed + summary.expired,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'triangleExclamation',
|
icon: 'lucide:TriangleAlert',
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -211,7 +211,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
.gridActions=${[
|
.gridActions=${[
|
||||||
{
|
{
|
||||||
name: 'Refresh',
|
name: 'Refresh',
|
||||||
iconName: 'arrowsRotate',
|
iconName: 'lucide:RefreshCw',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await appstate.certificateStatePart.dispatchAction(
|
await appstate.certificateStatePart.dispatchAction(
|
||||||
appstate.fetchCertificateOverviewAction,
|
appstate.fetchCertificateOverviewAction,
|
||||||
@@ -243,7 +243,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'Reprovision',
|
name: 'Reprovision',
|
||||||
iconName: 'arrowsRotate',
|
iconName: 'lucide:RefreshCw',
|
||||||
type: ['inRow'],
|
type: ['inRow'],
|
||||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||||
const cert = actionData.item;
|
const cert = actionData.item;
|
||||||
@@ -270,7 +270,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
iconName: 'magnifyingGlass',
|
iconName: 'lucide:Search',
|
||||||
type: ['doubleClick', 'contextmenu'],
|
type: ['doubleClick', 'contextmenu'],
|
||||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||||
const cert = actionData.item;
|
const cert = actionData.item;
|
||||||
@@ -289,7 +289,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Copy Domain',
|
name: 'Copy Domain',
|
||||||
iconName: 'copy',
|
iconName: 'lucide:Copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(cert.domain);
|
await navigator.clipboard.writeText(cert.domain);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
iconName: 'magnifyingGlass',
|
iconName: 'lucide:Search',
|
||||||
type: ['inRow', 'doubleClick', 'contextmenu'],
|
type: ['inRow', 'doubleClick', 'contextmenu'],
|
||||||
actionFunc: async (actionData) => {
|
actionFunc: async (actionData) => {
|
||||||
await this.showRequestDetails(actionData.item);
|
await this.showRequestDetails(actionData.item);
|
||||||
@@ -336,7 +336,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Copy Request ID',
|
name: 'Copy Request ID',
|
||||||
iconName: 'copy',
|
iconName: 'lucide:Copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(request.id);
|
await navigator.clipboard.writeText(request.id);
|
||||||
}
|
}
|
||||||
@@ -429,13 +429,13 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
title: 'Active Connections',
|
title: 'Active Connections',
|
||||||
value: activeConnections,
|
value: activeConnections,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'plug',
|
icon: 'lucide:Plug',
|
||||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||||
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
iconName: 'magnifyingGlass',
|
iconName: 'lucide:Search',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -446,7 +446,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
title: 'Requests/sec',
|
title: 'Requests/sec',
|
||||||
value: reqPerSec,
|
value: reqPerSec,
|
||||||
type: 'trend',
|
type: 'trend',
|
||||||
icon: 'chartLine',
|
icon: 'lucide:ChartLine',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
trendData: trendData,
|
trendData: trendData,
|
||||||
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
||||||
@@ -457,7 +457,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
value: this.formatBitsPerSecond(throughput.in),
|
value: this.formatBitsPerSecond(throughput.in),
|
||||||
unit: '',
|
unit: '',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'download',
|
icon: 'lucide:Download',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -467,7 +467,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
value: this.formatBitsPerSecond(throughput.out),
|
value: this.formatBitsPerSecond(throughput.out),
|
||||||
unit: '',
|
unit: '',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'upload',
|
icon: 'lucide:Upload',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -480,7 +480,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
.gridActions=${[
|
.gridActions=${[
|
||||||
{
|
{
|
||||||
name: 'Export Data',
|
name: 'Export Data',
|
||||||
iconName: 'fileExport',
|
iconName: 'lucide:FileOutput',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
console.log('Export feature coming soon');
|
console.log('Export feature coming soon');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Server Status',
|
title: 'Server Status',
|
||||||
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
|
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
icon: 'server',
|
icon: 'lucide:Server',
|
||||||
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
|
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
|
||||||
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
|
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
|
||||||
},
|
},
|
||||||
@@ -172,7 +172,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Active Connections',
|
title: 'Active Connections',
|
||||||
value: this.statsState.serverStats.activeConnections,
|
value: this.statsState.serverStats.activeConnections,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'networkWired',
|
icon: 'lucide:Network',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
||||||
},
|
},
|
||||||
@@ -181,7 +181,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Throughput In',
|
title: 'Throughput In',
|
||||||
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
|
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
icon: 'download',
|
icon: 'lucide:Download',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
|
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -190,7 +190,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Throughput Out',
|
title: 'Throughput Out',
|
||||||
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
|
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
icon: 'upload',
|
icon: 'lucide:Upload',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
|
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -199,7 +199,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'CPU Usage',
|
title: 'CPU Usage',
|
||||||
value: cpuUsage,
|
value: cpuUsage,
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
icon: 'microchip',
|
icon: 'lucide:Cpu',
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -215,7 +215,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Memory Usage',
|
title: 'Memory Usage',
|
||||||
value: memoryUsage,
|
value: memoryUsage,
|
||||||
type: 'percentage',
|
type: 'percentage',
|
||||||
icon: 'memory',
|
icon: 'lucide:MemoryStick',
|
||||||
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
|
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
|
||||||
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
|
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
|
||||||
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
|
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
|
||||||
@@ -229,7 +229,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
.gridActions=${[
|
.gridActions=${[
|
||||||
{
|
{
|
||||||
name: 'Refresh',
|
name: 'Refresh',
|
||||||
iconName: 'arrowsRotate',
|
iconName: 'lucide:RefreshCw',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||||
},
|
},
|
||||||
@@ -251,7 +251,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Emails Sent',
|
title: 'Emails Sent',
|
||||||
value: this.statsState.emailStats.sent,
|
value: this.statsState.emailStats.sent,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'paperPlane',
|
icon: 'lucide:Send',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
|
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
|
||||||
},
|
},
|
||||||
@@ -260,7 +260,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Emails Received',
|
title: 'Emails Received',
|
||||||
value: this.statsState.emailStats.received,
|
value: this.statsState.emailStats.received,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'envelope',
|
icon: 'lucide:Mail',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -268,7 +268,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Queued',
|
title: 'Queued',
|
||||||
value: this.statsState.emailStats.queued,
|
value: this.statsState.emailStats.queued,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'clock',
|
icon: 'lucide:Clock',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
description: 'Pending delivery',
|
description: 'Pending delivery',
|
||||||
},
|
},
|
||||||
@@ -277,7 +277,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Failed',
|
title: 'Failed',
|
||||||
value: this.statsState.emailStats.failed,
|
value: this.statsState.emailStats.failed,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'triangleExclamation',
|
icon: 'lucide:TriangleAlert',
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
|
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
|
||||||
},
|
},
|
||||||
@@ -300,7 +300,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'DNS Queries',
|
title: 'DNS Queries',
|
||||||
value: this.statsState.dnsStats.totalQueries,
|
value: this.statsState.dnsStats.totalQueries,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'globe',
|
icon: 'lucide:Globe',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
description: 'Total queries handled',
|
description: 'Total queries handled',
|
||||||
},
|
},
|
||||||
@@ -309,7 +309,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Cache Hit Rate',
|
title: 'Cache Hit Rate',
|
||||||
value: cacheHitRate,
|
value: cacheHitRate,
|
||||||
type: 'percentage',
|
type: 'percentage',
|
||||||
icon: 'database',
|
icon: 'lucide:Database',
|
||||||
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
|
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
|
||||||
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
|
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
|
||||||
},
|
},
|
||||||
@@ -318,7 +318,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Active Domains',
|
title: 'Active Domains',
|
||||||
value: this.statsState.dnsStats.activeDomains,
|
value: this.statsState.dnsStats.activeDomains,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'sitemap',
|
icon: 'lucide:Network',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -327,7 +327,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
|
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
|
||||||
unit: 'ms',
|
unit: 'ms',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'clockRotateLeft',
|
icon: 'lucide:History',
|
||||||
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
|
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
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')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -187,7 +198,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
name: edge.name,
|
name: edge.name,
|
||||||
status: this.getEdgeStatusHtml(edge),
|
status: this.getEdgeStatusHtml(edge),
|
||||||
publicIp: this.getEdgePublicIp(edge.id),
|
publicIp: this.getEdgePublicIp(edge.id),
|
||||||
ports: this.getPortsHtml(edge.listenPorts),
|
ports: this.getPortsHtml(edge),
|
||||||
tunnels: this.getEdgeTunnelCount(edge.id),
|
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||||
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
||||||
})}
|
})}
|
||||||
@@ -198,42 +209,137 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
type: ['header'],
|
type: ['header'],
|
||||||
actionFunc: async () => {
|
actionFunc: async () => {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
const result = await DeesModal.createAndShow({
|
const modal = await DeesModal.createAndShow({
|
||||||
heading: 'Create Edge Node',
|
heading: 'Create Edge Node',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||||
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated)'} .required=${true} .value=${'443,25'}></dees-input-text>
|
<dees-input-text .key=${'listenPorts'} .label=${'Additional Manual Ports (comma-separated, optional)'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
|
||||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [],
|
menuOptions: [
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
const formData = result as any;
|
|
||||||
const ports = (formData.name ? formData.listenPorts : '443')
|
|
||||||
.split(',')
|
|
||||||
.map((p: string) => parseInt(p.trim(), 10))
|
|
||||||
.filter((p: number) => !isNaN(p));
|
|
||||||
const tags = formData.tags
|
|
||||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
|
||||||
: undefined;
|
|
||||||
await appstate.remoteIngressStatePart.dispatchAction(
|
|
||||||
appstate.createRemoteIngressAction,
|
|
||||||
{
|
{
|
||||||
name: formData.name,
|
name: 'Cancel',
|
||||||
listenPorts: ports,
|
iconName: 'lucide:x',
|
||||||
tags,
|
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;
|
||||||
|
const tags = formData.tags
|
||||||
|
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.createRemoteIngressAction,
|
||||||
|
{ name, listenPorts, autoDerivePorts, tags },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports (comma-separated)'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated)'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Regenerate Secret',
|
name: 'Regenerate Secret',
|
||||||
iconName: 'lucide:key',
|
iconName: 'lucide:key',
|
||||||
type: ['row'],
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
action: async (edge: interfaces.data.IRemoteIngress) => {
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
await appstate.remoteIngressStatePart.dispatchAction(
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
appstate.regenerateRemoteIngressSecretAction,
|
appstate.regenerateRemoteIngressSecretAction,
|
||||||
edge.id,
|
edge.id,
|
||||||
@@ -243,8 +349,9 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
{
|
{
|
||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
iconName: 'lucide:trash2',
|
iconName: 'lucide:trash2',
|
||||||
type: ['row'],
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
action: async (edge: interfaces.data.IRemoteIngress) => {
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
await appstate.remoteIngressStatePart.dispatchAction(
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
appstate.deleteRemoteIngressAction,
|
appstate.deleteRemoteIngressAction,
|
||||||
edge.id,
|
edge.id,
|
||||||
@@ -277,8 +384,13 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
return status?.publicIp || '-';
|
return status?.publicIp || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPortsHtml(ports: number[]): TemplateResult {
|
private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
|
||||||
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
|
const manualPorts = edge.manualPorts || [];
|
||||||
|
const derivedPorts = edge.derivedPorts || [];
|
||||||
|
if (manualPorts.length === 0 && derivedPorts.length === 0) {
|
||||||
|
return html`<span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span>`;
|
||||||
|
}
|
||||||
|
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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEdgeTunnelCount(edgeId: string): number {
|
private getEdgeTunnelCount(edgeId: string): number {
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Threat Level',
|
title: 'Threat Level',
|
||||||
value: threatScore,
|
value: threatScore,
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
icon: 'shield',
|
icon: 'lucide:Shield',
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -273,7 +273,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Blocked Threats',
|
title: 'Blocked Threats',
|
||||||
value: metrics.blockedIPs.length + metrics.spamDetected,
|
value: metrics.blockedIPs.length + metrics.spamDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'userShield',
|
icon: 'lucide:ShieldCheck',
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
description: 'Total threats blocked today',
|
description: 'Total threats blocked today',
|
||||||
},
|
},
|
||||||
@@ -282,7 +282,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Active Sessions',
|
title: 'Active Sessions',
|
||||||
value: 0,
|
value: 0,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'users',
|
icon: 'lucide:Users',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: 'Current authenticated sessions',
|
description: 'Current authenticated sessions',
|
||||||
},
|
},
|
||||||
@@ -291,7 +291,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Auth Failures',
|
title: 'Auth Failures',
|
||||||
value: metrics.authenticationFailures,
|
value: metrics.authenticationFailures,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lockOpen',
|
icon: 'lucide:LockOpen',
|
||||||
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||||||
description: 'Failed login attempts today',
|
description: 'Failed login attempts today',
|
||||||
},
|
},
|
||||||
@@ -355,7 +355,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Authentication Failures',
|
title: 'Authentication Failures',
|
||||||
value: metrics.authenticationFailures,
|
value: metrics.authenticationFailures,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lockOpen',
|
icon: 'lucide:LockOpen',
|
||||||
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||||||
description: 'Failed authentication attempts today',
|
description: 'Failed authentication attempts today',
|
||||||
},
|
},
|
||||||
@@ -364,7 +364,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Successful Logins',
|
title: 'Successful Logins',
|
||||||
value: 0,
|
value: 0,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lock',
|
icon: 'lucide:Lock',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: 'Successful logins today',
|
description: 'Successful logins today',
|
||||||
},
|
},
|
||||||
@@ -399,7 +399,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Malware Detection',
|
title: 'Malware Detection',
|
||||||
value: metrics.malwareDetected,
|
value: metrics.malwareDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'virusSlash',
|
icon: 'lucide:BugOff',
|
||||||
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
|
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
|
||||||
description: 'Malware detected',
|
description: 'Malware detected',
|
||||||
},
|
},
|
||||||
@@ -408,7 +408,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Phishing Detection',
|
title: 'Phishing Detection',
|
||||||
value: metrics.phishingDetected,
|
value: metrics.phishingDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'fishFins',
|
icon: 'lucide:Fish',
|
||||||
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
|
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
|
||||||
description: 'Phishing attempts detected',
|
description: 'Phishing attempts detected',
|
||||||
},
|
},
|
||||||
@@ -417,7 +417,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Suspicious Activities',
|
title: 'Suspicious Activities',
|
||||||
value: metrics.suspiciousActivities,
|
value: metrics.suspiciousActivities,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'triangleExclamation',
|
icon: 'lucide:TriangleAlert',
|
||||||
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
|
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
|
||||||
description: 'Suspicious activities detected',
|
description: 'Suspicious activities detected',
|
||||||
},
|
},
|
||||||
@@ -426,7 +426,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Spam Detection',
|
title: 'Spam Detection',
|
||||||
value: metrics.spamDetected,
|
value: metrics.spamDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'ban',
|
icon: 'lucide:Ban',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
description: 'Spam emails blocked',
|
description: 'Spam emails blocked',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user