Compare commits

..

8 Commits
v6.5.0 ... main

Author SHA1 Message Date
529a4bae00 v6.8.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 14:17:18 +00:00
49606ae007 feat(remote-ingress): support auto-deriving ports for remote ingress edges and expose manual/derived port breakdown in API and UI 2026-02-17 14:17:18 +00:00
31a6510d8b v6.7.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 11:56:54 +00:00
b5e760ae07 feat(remote-ingress): Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI 2026-02-17 11:56:54 +00:00
ea32babaac v6.6.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 10:57:27 +00:00
a4ddedaf46 fix(icons): standardize icon identifiers to lucide-prefixed names across operational views 2026-02-17 10:57:27 +00:00
7ce09c53ca v6.6.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 10:55:31 +00:00
69be2295f1 feat(remoteingress): derive effective remote ingress listen ports from route configs and expose them via ops API 2026-02-17 10:55:31 +00:00
16 changed files with 456 additions and 90 deletions

View File

@@ -1,5 +1,40 @@
# 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)
derive effective remote ingress listen ports from route configs and expose them via ops API
- Derive listen ports from SmartProxy route configs with remoteIngress.enabled; supports optional edgeFilter to target edges by id or tags.
- Add RemoteIngressManager.setRoutes(), derivePortsForEdge(), and getEffectiveListenPorts() which falls back to manual listenPorts when present.
- dcrouter now supplies route configs to RemoteIngressManager during initialization and when updating SmartProxy configuration to keep derived ports in sync.
- Ops API now returns effectiveListenPorts for edges; createRemoteIngress.listenPorts is optional and createEdge defaults listenPorts to an empty array.
- Bump dependency @serve.zone/remoteingress to ^3.0.4 to align types/behavior.
## 2026-02-16 - 6.5.0 - feat(ops-view-remoteingress)
add 'Create Edge Node' header action to remote ingress table and remove duplicate createNewAction

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "6.5.0",
"version": "6.8.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -56,7 +56,7 @@
"@push.rocks/smartstate": "^2.0.30",
"@push.rocks/smartunique": "^3.0.9",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^3.0.2",
"@serve.zone/remoteingress": "^3.0.4",
"@tsclass/tsclass": "^9.3.0",
"lru-cache": "^11.2.6",
"uuid": "^13.0.0"

10
pnpm-lock.yaml generated
View File

@@ -96,8 +96,8 @@ importers:
specifier: ^5.3.0
version: 5.3.0
'@serve.zone/remoteingress':
specifier: ^3.0.2
version: 3.0.2
specifier: ^3.0.4
version: 3.0.4
'@tsclass/tsclass':
specifier: ^9.3.0
version: 9.3.0
@@ -1340,8 +1340,8 @@ packages:
'@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
'@serve.zone/remoteingress@3.0.2':
resolution: {integrity: sha512-FnwNVO0Dn9xuNv0t81u6pjCizSeCyMjkRKm6wN5qycCdGFoJmFbBamHqV01KtK1KcgDTd7LX+PowSqKReNrBGw==}
'@serve.zone/remoteingress@3.0.4':
resolution: {integrity: sha512-ZD66Y8fvW7SjealziOlhaC7+Y/3gxQkZlj/X8rxgVHmGhlc/YQtn6H6LNVazbM88BXK5ns004Qo6ongAB6Ho0Q==}
'@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -6830,7 +6830,7 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.3.0
'@serve.zone/remoteingress@3.0.2':
'@serve.zone/remoteingress@3.0.4':
dependencies:
'@push.rocks/qenv': 6.1.3
'@push.rocks/smartrust': 1.2.1

View File

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

View File

@@ -956,6 +956,11 @@ export class DcRouter {
// Update configuration
this.options.smartProxyConfig = config;
// Update routes on RemoteIngressManager so derived ports stay in sync
if (this.remoteIngressManager && config.routes) {
this.remoteIngressManager.setRoutes(config.routes as any[]);
}
// Start new SmartProxy with updated configuration (will include email routes if configured)
await this.setupSmartProxy();
@@ -1587,6 +1592,10 @@ export class DcRouter {
this.remoteIngressManager = new RemoteIngressManager(this.storageManager);
await this.remoteIngressManager.initialize();
// Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
const currentRoutes = this.options.smartProxyConfig?.routes || [];
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
// Create and start the tunnel manager
this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
tunnelPort: this.options.remoteIngressConfig.tunnelPort ?? 8443,

View File

@@ -20,11 +20,17 @@ export class RemoteIngressHandler {
if (!manager) {
return { edges: [] };
}
// Return edges without secrets
const edges = manager.getAllEdges().map((e) => ({
// Return edges without secrets, enriched with effective listen ports and breakdown
const edges = manager.getAllEdges().map((e) => {
const breakdown = manager.getPortBreakdown(e);
return {
...e,
secret: '********', // Never expose secrets via API
}));
effectiveListenPorts: manager.getEffectiveListenPorts(e),
manualPorts: breakdown.manual,
derivedPorts: breakdown.derived,
};
});
return { edges };
},
),
@@ -47,8 +53,9 @@ export class RemoteIngressHandler {
const edge = await manager.createEdge(
dataArg.name,
dataArg.listenPorts,
dataArg.listenPorts || [],
dataArg.tags,
dataArg.autoDerivePorts ?? true,
);
// Sync allowed edges with the hub
@@ -101,6 +108,7 @@ export class RemoteIngressHandler {
const edge = await manager.updateEdge(dataArg.id, {
name: dataArg.name,
listenPorts: dataArg.listenPorts,
autoDerivePorts: dataArg.autoDerivePorts,
enabled: dataArg.enabled,
tags: dataArg.tags,
});
@@ -114,7 +122,17 @@ export class RemoteIngressHandler {
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,
},
};
},
),
);

View File

@@ -1,9 +1,30 @@
import * as plugins from '../plugins.js';
import type { StorageManager } from '../storage/classes.storagemanager.js';
import type { IRemoteIngress } from '../../ts_interfaces/data/remoteingress.js';
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
const STORAGE_PREFIX = '/remote-ingress/';
/**
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
*/
function extractPorts(portRange: number | number[] | Array<{ from: number; to: number }>): number[] {
const ports = new Set<number>();
if (typeof portRange === 'number') {
ports.add(portRange);
} else if (Array.isArray(portRange)) {
for (const entry of portRange) {
if (typeof entry === 'number') {
ports.add(entry);
} else if (typeof entry === 'object' && 'from' in entry && 'to' in entry) {
for (let p = entry.from; p <= entry.to; p++) {
ports.add(p);
}
}
}
}
return [...ports].sort((a, b) => a - b);
}
/**
* Manages CRUD for remote ingress edge registrations.
* Persists edge configs via StorageManager and provides
@@ -12,6 +33,7 @@ const STORAGE_PREFIX = '/remote-ingress/';
export class RemoteIngressManager {
private storageManager: StorageManager;
private edges: Map<string, IRemoteIngress> = new Map();
private routes: IDcRouterRouteConfig[] = [];
constructor(storageManager: StorageManager) {
this.storageManager = storageManager;
@@ -25,18 +47,87 @@ export class RemoteIngressManager {
for (const key of keys) {
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
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);
}
}
}
/**
* Store the current route configs for port derivation.
*/
public setRoutes(routes: IDcRouterRouteConfig[]): void {
this.routes = routes;
}
/**
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
* When edgeFilter is absent, the route applies to all edges.
*/
public derivePortsForEdge(edgeId: string, edgeTags?: string[]): number[] {
const ports = new Set<number>();
for (const route of this.routes) {
if (!route.remoteIngress?.enabled) continue;
// Apply edge filter if present
const filter = route.remoteIngress.edgeFilter;
if (filter && filter.length > 0) {
const idMatch = filter.includes(edgeId);
const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false;
if (!idMatch && !tagMatch) continue;
}
// Extract ports from the route match
if (route.match?.ports) {
for (const p of extractPorts(route.match.ports)) {
ports.add(p);
}
}
}
return [...ports].sort((a, b) => a - b);
}
/**
* Get the effective listen ports for an edge.
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
*/
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
const manualPorts = edge.listenPorts || [];
const shouldDerive = edge.autoDerivePorts !== false;
if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
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 };
}
/**
* Create a new edge registration.
*/
public async createEdge(
name: string,
listenPorts: number[],
listenPorts: number[] = [],
tags?: string[],
autoDerivePorts: boolean = true,
): Promise<IRemoteIngress> {
const id = plugins.uuid.v4();
const secret = plugins.crypto.randomBytes(32).toString('hex');
@@ -48,6 +139,7 @@ export class RemoteIngressManager {
secret,
listenPorts,
enabled: true,
autoDerivePorts,
tags: tags || [],
createdAt: now,
updatedAt: now,
@@ -80,6 +172,7 @@ export class RemoteIngressManager {
updates: {
name?: string;
listenPorts?: number[];
autoDerivePorts?: boolean;
enabled?: boolean;
tags?: string[];
},
@@ -91,6 +184,7 @@ export class RemoteIngressManager {
if (updates.name !== undefined) edge.name = updates.name;
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.tags !== undefined) edge.tags = updates.tags;
edge.updatedAt = Date.now();

View File

@@ -1,3 +1,5 @@
import type { IRouteConfig } from '@push.rocks/smartproxy';
/**
* A stored remote ingress edge registration.
*/
@@ -7,9 +9,17 @@ export interface IRemoteIngress {
secret: string;
listenPorts: number[];
enabled: boolean;
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
autoDerivePorts: boolean;
tags?: string[];
createdAt: 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[];
}
/**
@@ -23,3 +33,25 @@ export interface IRemoteIngressStatus {
lastHeartbeat: number | null;
connectedAt: number | null;
}
/**
* Route-level remote ingress configuration.
* When attached to a route, signals that traffic for this route
* should be accepted from remote edge nodes.
*/
export interface IRouteRemoteIngress {
/** Whether this route receives traffic from edge nodes */
enabled: boolean;
/** Optional filter: only edges whose id or tags match get this route's ports.
* When absent, the route applies to all edges. */
edgeFilter?: string[];
}
/**
* Extended route config used within dcrouter.
* Adds the optional `remoteIngress` property to SmartProxy's IRouteConfig.
* SmartProxy ignores unknown properties at runtime.
*/
export type IDcRouterRouteConfig = IRouteConfig & {
remoteIngress?: IRouteRemoteIngress;
};

View File

@@ -17,7 +17,8 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
request: {
identity?: authInterfaces.IIdentity;
name: string;
listenPorts: number[];
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
};
response: {
@@ -57,6 +58,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
id: string;
name?: string;
listenPorts?: number[];
autoDerivePorts?: boolean;
enabled?: boolean;
tags?: string[];
};

View File

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

View File

@@ -821,7 +821,8 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
name: string;
listenPorts: number[];
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
@@ -836,6 +837,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
identity: context.identity,
name: dataArg.name,
listenPorts: dataArg.listenPorts,
autoDerivePorts: dataArg.autoDerivePorts,
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>(
async (statePartArg, edgeId) => {
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
async function dispatchCombinedRefreshAction() {
const context = getActionContext();

View File

@@ -175,7 +175,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Total Certificates',
value: summary.total,
type: 'number',
icon: 'shieldHalved',
icon: 'lucide:ShieldHalf',
color: '#3b82f6',
},
{
@@ -183,7 +183,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Valid',
value: summary.valid,
type: 'number',
icon: 'check',
icon: 'lucide:Check',
color: '#22c55e',
},
{
@@ -191,7 +191,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Expiring Soon',
value: summary.expiring,
type: 'number',
icon: 'clock',
icon: 'lucide:Clock',
color: '#f59e0b',
},
{
@@ -199,7 +199,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Failed / Expired',
value: summary.failed + summary.expired,
type: 'number',
icon: 'triangleExclamation',
icon: 'lucide:TriangleAlert',
color: '#ef4444',
},
];
@@ -211,7 +211,7 @@ export class OpsViewCertificates extends DeesElement {
.gridActions=${[
{
name: 'Refresh',
iconName: 'arrowsRotate',
iconName: 'lucide:RefreshCw',
action: async () => {
await appstate.certificateStatePart.dispatchAction(
appstate.fetchCertificateOverviewAction,
@@ -243,7 +243,7 @@ export class OpsViewCertificates extends DeesElement {
.dataActions=${[
{
name: 'Reprovision',
iconName: 'arrowsRotate',
iconName: 'lucide:RefreshCw',
type: ['inRow'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item;
@@ -270,7 +270,7 @@ export class OpsViewCertificates extends DeesElement {
},
{
name: 'View Details',
iconName: 'magnifyingGlass',
iconName: 'lucide:Search',
type: ['doubleClick', 'contextmenu'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item;
@@ -289,7 +289,7 @@ export class OpsViewCertificates extends DeesElement {
menuOptions: [
{
name: 'Copy Domain',
iconName: 'copy',
iconName: 'lucide:Copy',
action: async () => {
await navigator.clipboard.writeText(cert.domain);
},

View File

@@ -287,7 +287,7 @@ export class OpsViewNetwork extends DeesElement {
.dataActions=${[
{
name: 'View Details',
iconName: 'magnifyingGlass',
iconName: 'lucide:Search',
type: ['inRow', 'doubleClick', 'contextmenu'],
actionFunc: async (actionData) => {
await this.showRequestDetails(actionData.item);
@@ -336,7 +336,7 @@ export class OpsViewNetwork extends DeesElement {
menuOptions: [
{
name: 'Copy Request ID',
iconName: 'copy',
iconName: 'lucide:Copy',
action: async () => {
await navigator.clipboard.writeText(request.id);
}
@@ -429,13 +429,13 @@ export class OpsViewNetwork extends DeesElement {
title: 'Active Connections',
value: activeConnections,
type: 'number',
icon: 'plug',
icon: 'lucide:Plug',
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
actions: [
{
name: 'View Details',
iconName: 'magnifyingGlass',
iconName: 'lucide:Search',
action: async () => {
},
},
@@ -446,7 +446,7 @@ export class OpsViewNetwork extends DeesElement {
title: 'Requests/sec',
value: reqPerSec,
type: 'trend',
icon: 'chartLine',
icon: 'lucide:ChartLine',
color: '#3b82f6',
trendData: trendData,
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
@@ -457,7 +457,7 @@ export class OpsViewNetwork extends DeesElement {
value: this.formatBitsPerSecond(throughput.in),
unit: '',
type: 'number',
icon: 'download',
icon: 'lucide:Download',
color: '#22c55e',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
},
@@ -467,7 +467,7 @@ export class OpsViewNetwork extends DeesElement {
value: this.formatBitsPerSecond(throughput.out),
unit: '',
type: 'number',
icon: 'upload',
icon: 'lucide:Upload',
color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
},
@@ -480,7 +480,7 @@ export class OpsViewNetwork extends DeesElement {
.gridActions=${[
{
name: 'Export Data',
iconName: 'fileExport',
iconName: 'lucide:FileOutput',
action: async () => {
console.log('Export feature coming soon');
},

View File

@@ -163,7 +163,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Server Status',
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
type: 'text',
icon: 'server',
icon: 'lucide:Server',
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
},
@@ -172,7 +172,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Active Connections',
value: this.statsState.serverStats.activeConnections,
type: 'number',
icon: 'networkWired',
icon: 'lucide:Network',
color: '#3b82f6',
description: `Total: ${this.statsState.serverStats.totalConnections}`,
},
@@ -181,7 +181,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Throughput In',
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
type: 'text',
icon: 'download',
icon: 'lucide:Download',
color: '#22c55e',
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
},
@@ -190,7 +190,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Throughput Out',
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
type: 'text',
icon: 'upload',
icon: 'lucide:Upload',
color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
},
@@ -199,7 +199,7 @@ export class OpsViewOverview extends DeesElement {
title: 'CPU Usage',
value: cpuUsage,
type: 'gauge',
icon: 'microchip',
icon: 'lucide:Cpu',
gaugeOptions: {
min: 0,
max: 100,
@@ -215,7 +215,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Memory Usage',
value: memoryUsage,
type: 'percentage',
icon: 'memory',
icon: 'lucide:MemoryStick',
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
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)}`
@@ -229,7 +229,7 @@ export class OpsViewOverview extends DeesElement {
.gridActions=${[
{
name: 'Refresh',
iconName: 'arrowsRotate',
iconName: 'lucide:RefreshCw',
action: async () => {
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
},
@@ -251,7 +251,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Emails Sent',
value: this.statsState.emailStats.sent,
type: 'number',
icon: 'paperPlane',
icon: 'lucide:Send',
color: '#22c55e',
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
},
@@ -260,7 +260,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Emails Received',
value: this.statsState.emailStats.received,
type: 'number',
icon: 'envelope',
icon: 'lucide:Mail',
color: '#3b82f6',
},
{
@@ -268,7 +268,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Queued',
value: this.statsState.emailStats.queued,
type: 'number',
icon: 'clock',
icon: 'lucide:Clock',
color: '#f59e0b',
description: 'Pending delivery',
},
@@ -277,7 +277,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Failed',
value: this.statsState.emailStats.failed,
type: 'number',
icon: 'triangleExclamation',
icon: 'lucide:TriangleAlert',
color: '#ef4444',
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
},
@@ -300,7 +300,7 @@ export class OpsViewOverview extends DeesElement {
title: 'DNS Queries',
value: this.statsState.dnsStats.totalQueries,
type: 'number',
icon: 'globe',
icon: 'lucide:Globe',
color: '#3b82f6',
description: 'Total queries handled',
},
@@ -309,7 +309,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Cache Hit Rate',
value: cacheHitRate,
type: 'percentage',
icon: 'database',
icon: 'lucide:Database',
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
},
@@ -318,7 +318,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Active Domains',
value: this.statsState.dnsStats.activeDomains,
type: 'number',
icon: 'sitemap',
icon: 'lucide:Network',
color: '#8b5cf6',
},
{
@@ -327,7 +327,7 @@ export class OpsViewOverview extends DeesElement {
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
unit: 'ms',
type: 'number',
icon: 'clockRotateLeft',
icon: 'lucide:History',
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
},
];

View File

@@ -114,6 +114,17 @@ export class OpsViewRemoteIngress extends DeesElement {
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
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,
status: this.getEdgeStatusHtml(edge),
publicIp: this.getEdgePublicIp(edge.id),
ports: this.getPortsHtml(edge.listenPorts),
ports: this.getPortsHtml(edge),
tunnels: this.getEdgeTunnelCount(edge.id),
lastHeartbeat: this.getLastHeartbeat(edge.id),
})}
@@ -198,42 +209,137 @@ export class OpsViewRemoteIngress extends DeesElement {
type: ['header'],
actionFunc: async () => {
const { DeesModal } = await import('@design.estate/dees-catalog');
const result = await DeesModal.createAndShow({
const modal = await DeesModal.createAndShow({
heading: 'Create Edge Node',
content: html`
<dees-form>
<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-form>
`,
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));
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 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: formData.name,
listenPorts: ports,
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',
iconName: 'lucide:key',
type: ['row'],
action: async (edge: interfaces.data.IRemoteIngress) => {
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.regenerateRemoteIngressSecretAction,
edge.id,
@@ -243,8 +349,9 @@ export class OpsViewRemoteIngress extends DeesElement {
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['row'],
action: async (edge: interfaces.data.IRemoteIngress) => {
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.deleteRemoteIngressAction,
edge.id,
@@ -277,8 +384,13 @@ export class OpsViewRemoteIngress extends DeesElement {
return status?.publicIp || '-';
}
private getPortsHtml(ports: number[]): TemplateResult {
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
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 {

View File

@@ -256,7 +256,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Threat Level',
value: threatScore,
type: 'gauge',
icon: 'shield',
icon: 'lucide:Shield',
gaugeOptions: {
min: 0,
max: 100,
@@ -273,7 +273,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Blocked Threats',
value: metrics.blockedIPs.length + metrics.spamDetected,
type: 'number',
icon: 'userShield',
icon: 'lucide:ShieldCheck',
color: '#ef4444',
description: 'Total threats blocked today',
},
@@ -282,7 +282,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Active Sessions',
value: 0,
type: 'number',
icon: 'users',
icon: 'lucide:Users',
color: '#22c55e',
description: 'Current authenticated sessions',
},
@@ -291,7 +291,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Auth Failures',
value: metrics.authenticationFailures,
type: 'number',
icon: 'lockOpen',
icon: 'lucide:LockOpen',
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
description: 'Failed login attempts today',
},
@@ -355,7 +355,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Authentication Failures',
value: metrics.authenticationFailures,
type: 'number',
icon: 'lockOpen',
icon: 'lucide:LockOpen',
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
description: 'Failed authentication attempts today',
},
@@ -364,7 +364,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Successful Logins',
value: 0,
type: 'number',
icon: 'lock',
icon: 'lucide:Lock',
color: '#22c55e',
description: 'Successful logins today',
},
@@ -399,7 +399,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Malware Detection',
value: metrics.malwareDetected,
type: 'number',
icon: 'virusSlash',
icon: 'lucide:BugOff',
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
description: 'Malware detected',
},
@@ -408,7 +408,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Phishing Detection',
value: metrics.phishingDetected,
type: 'number',
icon: 'fishFins',
icon: 'lucide:Fish',
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
description: 'Phishing attempts detected',
},
@@ -417,7 +417,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Suspicious Activities',
value: metrics.suspiciousActivities,
type: 'number',
icon: 'triangleExclamation',
icon: 'lucide:TriangleAlert',
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
description: 'Suspicious activities detected',
},
@@ -426,7 +426,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Spam Detection',
value: metrics.spamDetected,
type: 'number',
icon: 'ban',
icon: 'lucide:Ban',
color: '#f59e0b',
description: 'Spam emails blocked',
},