Compare commits

..

12 Commits

Author SHA1 Message Date
b62a322c54 v12.5.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-03 08:19:02 +00:00
a3a64e9a02 fix(repo): no changes to commit 2026-04-03 08:19:02 +00:00
491e51f40b v12.5.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-03 08:18:28 +00:00
b46247d9cb fix(ops-view-network): centralize traffic chart timing constants for consistent rolling window updates 2026-04-03 08:18:28 +00:00
9c0e46ff4e v12.5.0
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 22:55:57 +00:00
f62bc4a526 feat(ops-view-routes): add priority support and list-based domain editing for routes 2026-04-02 22:55:57 +00:00
8f23600ec1 v12.4.0
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 22:37:49 +00:00
141f185fbf feat(routes): add route edit and delete actions to the ops routes view 2026-04-02 22:37:49 +00:00
6f4a5f19e7 v12.3.0
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 20:31:08 +00:00
9d8354e58f feat(docs,ops-dashboard): document unified database and reusable security profile and network target management 2026-04-02 20:31:08 +00:00
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
16 changed files with 465 additions and 148 deletions

View File

@@ -1,5 +1,46 @@
# Changelog
## 2026-04-03 - 12.5.2 - fix(repo)
no changes to commit
## 2026-04-03 - 12.5.1 - fix(ops-view-network)
centralize traffic chart timing constants for consistent rolling window updates
- Defines shared constants for the chart window, update interval, and maximum buffered data points
- Replaces hardcoded traffic history sizes and timer intervals with derived values across initialization, history loading, and live updates
- Keeps the chart rolling window configuration aligned with the in-memory traffic buffer
## 2026-04-02 - 12.5.0 - feat(ops-view-routes)
add priority support and list-based domain editing for routes
- Adds a priority field to route create and edit forms so route matching order can be configured.
- Replaces comma-separated domain text input with a list-based domain editor and updates form handling to persist domains as arrays.
## 2026-04-02 - 12.4.0 - feat(routes)
add route edit and delete actions to the ops routes view
- introduces an update route action in web app state and refreshes merged routes after changes
- adds edit and delete handlers with modal-based confirmation and route form inputs for programmatic routes
- enables realtime chart window configuration in network and overview dashboards
- bumps @serve.zone/catalog to ^2.11.0
## 2026-04-02 - 12.3.0 - feat(docs,ops-dashboard)
document unified database and reusable security profile and network target management
- Update project and interface documentation to replace separate storage/cache configuration with a unified database model
- Document new security profile and network target APIs, data models, and dashboard capabilities
- Add a global dashboard warning when the database is disabled so unavailable management features are clearly indicated
- Bump @design.estate/dees-catalog and @serve.zone/catalog to support the updated dashboard experience
## 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

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "12.2.5",
"version": "12.5.2",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -35,7 +35,7 @@
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.49.2",
"@design.estate/dees-catalog": "^3.50.2",
"@design.estate/dees-element": "^2.2.4",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.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.1",
"@serve.zone/catalog": "^2.11.0",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.15.3",
"@tsclass/tsclass": "^9.5.0",

28
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.2
version: 3.49.2(@tiptap/pm@2.27.2)
specifier: ^3.50.2
version: 3.50.2(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.2.4
version: 2.2.4
@@ -102,8 +102,8 @@ importers:
specifier: ^8.0.2
version: 8.0.2
'@serve.zone/catalog':
specifier: ^2.9.1
version: 2.9.1(@tiptap/pm@2.27.2)
specifier: ^2.11.0
version: 2.11.0(@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.2':
resolution: {integrity: sha512-ChVf5IW/w1WSsfuI3BA1SX2QJFjZljnAvnyPDXnbzTXuOdTgs054p66JwlDca9KM8yBlndwibgAYJfD6/4sONw==}
'@design.estate/dees-catalog@3.50.2':
resolution: {integrity: sha512-oxB1kB3IxEwHgf+DjytTBilkDVVb8hryq465OhhzgBiJiHaNLPyBASAQaNTVp6eaORQGzyCmy/ac/GdQglZiIg==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -1583,8 +1583,8 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@serve.zone/catalog@2.9.1':
resolution: {integrity: sha512-W+4x5O834DiEtcqfVNFrP6qYlj/6c9UTR9oEl2Wthf0R2SN0zC6Hbs16US2kP+mmQBKAIDMQYvTUI9oaXqvcog==}
'@serve.zone/catalog@2.11.0':
resolution: {integrity: sha512-4DFDewp1PFRhw5P+yQAoAw+i6gG2lfR3h+uPgbNxB5jCfW14eNDXi3nuwTMBQWRHL9jv8o0BokASjV9A0+q66g==}
'@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
@@ -4260,13 +4260,11 @@ 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==}
@@ -4341,7 +4339,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.2(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.50.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
@@ -4870,7 +4868,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.49.2(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.50.2(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
@@ -4978,7 +4976,7 @@ snapshots:
'@design.estate/dees-wcctools@3.8.0':
dependencies:
'@design.estate/dees-domtools': 2.5.3
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.2
@@ -6906,9 +6904,9 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
'@serve.zone/catalog@2.9.1(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.50.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

140
readme.md
View File

@@ -93,10 +93,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Socket-handler mode** — direct socket passing eliminates internal port hops
- **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput)
### 💾 Persistent Storage & Caching
- **Multiple storage backends**: filesystem, custom functions, or in-memory
- **Embedded cache database** via smartdata + smartdb (MongoDB-compatible)
### 💾 Unified Database
- **Two deployment modes**: embedded LocalSmartDb (zero-config) or external MongoDB
- **15 document classes** covering routes, certs, VPN, RADIUS, security profiles, network targets, and caches
- **Automatic TTL-based cleanup** for cached emails and IP reputation data
- **Reusable references** — security profiles and network targets that propagate changes to all referencing routes
### 🖥️ OpsServer Dashboard
- **Web-based management interface** with real-time monitoring
@@ -104,7 +105,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, VPN clients, and security events
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
- **Remote ingress management** with connection token generation and one-click copy
- **Read-only configuration display** — DcRouter is configured through code
- **Security profiles & network targets** — reusable security configurations and host:port targets with propagation to referencing routes
- **Global warning banners** when database is disabled (management features unavailable)
- **Read-only configuration display** for system overview
- **Smart tab visibility handling** — auto-pauses all polling, WebSocket connections, and chart updates when the browser tab is hidden, preventing resource waste and tab freezing
### 🔧 Programmatic API Client
@@ -269,11 +272,8 @@ const router = new DcRouter({
],
},
// Persistent storage
storage: { fsPath: '/var/lib/dcrouter/data' },
// Cache database
cacheConfig: { enabled: true, storagePath: '~/.serve.zone/dcrouter/tsmdb' },
// Unified database (embedded LocalSmartDb or external MongoDB)
dbConfig: { enabled: true },
// TLS & ACME
tls: { contactEmail: 'admin@example.com' },
@@ -311,8 +311,7 @@ graph TB
CM[Certificate Manager<br/><i>smartacme v9</i>]
OS[OpsServer Dashboard]
MM[Metrics Manager]
SM[Storage Manager]
CD[Cache Database]
DB2[DcRouterDb<br/><i>smartdata + smartdb</i>]
end
subgraph "Backend Services"
@@ -339,8 +338,7 @@ graph TB
DC --> CM
DC --> OS
DC --> MM
DC --> SM
DC --> CD
DC --> DB2
SP --> WEB
SP --> API
@@ -365,8 +363,7 @@ graph TB
| **RemoteIngress** | `@serve.zone/remoteingress` | Distributed edge tunneling with Rust data plane and TS management |
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
| **CacheDb** | `@push.rocks/smartdb` | Embedded MongoDB-compatible database (LocalSmartDb) for persistent caching |
| **DcRouterDb** | `@push.rocks/smartdata` + `@push.rocks/smartdb` | Unified database — embedded LocalSmartDb or external MongoDB for all persistence |
### How It Works
@@ -509,24 +506,16 @@ interface IDcRouterOptions {
};
dnsChallenge?: { cloudflareApiKey?: string };
// ── Storage & Caching ─────────────────────────────────────────
storage?: {
fsPath?: string;
readFunction?: (key: string) => Promise<string>;
writeFunction?: (key: string, value: string) => Promise<void>;
};
cacheConfig?: {
// ── Database ────────────────────────────────────────────────────
/** Unified database for all persistence (routes, certs, VPN, RADIUS, etc.) */
dbConfig?: {
enabled?: boolean; // default: true
mongoDbUrl?: string; // External MongoDB URL (omit for embedded LocalSmartDb)
storagePath?: string; // default: '~/.serve.zone/dcrouter/tsmdb'
dbName?: string; // default: 'dcrouter'
cleanupIntervalHours?: number; // default: 1
ttlConfig?: {
emails?: number; // default: 30 days
ipReputation?: number; // default: 1 day
bounces?: number; // default: 30 days
dkimKeys?: number; // default: 90 days
suppression?: number; // default: 30 days
};
seedOnEmpty?: boolean; // Seed default profiles/targets if DB is empty
seedData?: object; // Custom seed data
};
}
```
@@ -1213,49 +1202,55 @@ The OpsServer includes a **Certificates** view showing:
- One-click reprovisioning per domain
- Certificate import and export
## Storage & Caching
## Storage & Database
### StorageManager
DcRouter uses a **unified database** (`DcRouterDb`) powered by [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) + [`@push.rocks/smartdb`](https://code.foss.global/push.rocks/smartdb) for all persistence. It supports two modes:
Provides a unified key-value interface with three backends:
### Embedded LocalSmartDb (Default)
Zero-config, file-based MongoDB-compatible database — no external services needed:
```typescript
// Filesystem backend
storage: { fsPath: '/var/lib/dcrouter/data' }
// Custom backend (Redis, S3, etc.)
storage: {
readFunction: async (key) => await redis.get(key),
writeFunction: async (key, value) => await redis.set(key, value)
}
// In-memory (development only — data lost on restart)
// Simply omit the storage config
dbConfig: { enabled: true }
// Data stored at ~/.serve.zone/dcrouter/tsmdb by default
```
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state, remote ingress edge registrations.
### External MongoDB
### Cache Database
An embedded MongoDB-compatible database (via smartdata + smartdb) for persistent caching with automatic TTL cleanup:
Connect to an existing MongoDB instance:
```typescript
cacheConfig: {
dbConfig: {
enabled: true,
storagePath: '~/.serve.zone/dcrouter/tsmdb',
mongoDbUrl: 'mongodb://localhost:27017',
dbName: 'dcrouter',
cleanupIntervalHours: 1,
ttlConfig: {
emails: 30, // days
ipReputation: 1, // days
bounces: 30, // days
dkimKeys: 90, // days
suppression: 30 // days
}
}
```
Cached document types: `CachedEmail`, `CachedIPReputation`.
### Disabling the Database
For static, constructor-only deployments where no runtime management is needed:
```typescript
dbConfig: { enabled: false }
// Routes come exclusively from constructor config — no CRUD, no persistence
// OpsServer still runs but management features are disabled
```
### What's Stored
DcRouterDb persists all runtime state across 15 document classes:
| Category | Documents | Purpose |
|----------|-----------|---------|
| **Routes** | `StoredRouteDoc`, `RouteOverrideDoc` | Programmatic routes and hardcoded route overrides |
| **Certificates** | `ProxyCertDoc`, `AcmeCertDoc`, `CertBackoffDoc` | TLS certs, ACME state, per-domain backoff |
| **Auth** | `ApiTokenDoc` | API token storage |
| **Remote Ingress** | `RemoteIngressEdgeDoc` | Edge node registrations |
| **VPN** | `VpnServerKeysDoc`, `VpnClientDoc` | Server keys and client registrations |
| **RADIUS** | `VlanMappingsDoc`, `AccountingSessionDoc` | VLAN mappings and accounting sessions |
| **References** | `SecurityProfileDoc`, `NetworkTargetDoc` | Reusable security profiles and network targets |
| **Cache** | `CachedEmailDoc`, `CachedIpReputationDoc` | TTL-based caches with automatic cleanup |
## Security Features
@@ -1324,6 +1319,8 @@ The OpsServer provides a web-based management interface served on port 3000 by d
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning, import/export |
| 🌍 **RemoteIngress** | Edge node management, connection status, token generation, enable/disable |
| 🔐 **VPN** | VPN client management, server status, create/toggle/export/rotate/delete clients |
| 🛡️ **Security Profiles** | Reusable security configurations (IP allow/block lists, rate limits) |
| 🎯 **Network Targets** | Reusable host:port destinations for route references |
| 📡 **RADIUS** | NAS client management, VLAN mappings, session monitoring, accounting |
| 📜 **Logs** | Real-time log viewer with level filtering and search |
| ⚙️ **Configuration** | Read-only view of current system configuration |
@@ -1410,6 +1407,22 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'setVlanMapping' // Add/update VLAN mapping
'removeVlanMapping' // Remove VLAN mapping
'testVlanAssignment' // Test what VLAN a MAC gets
// Security Profiles
'getSecurityProfiles' // List all security profiles
'getSecurityProfile' // Get a single profile by ID
'createSecurityProfile' // Create a reusable security profile
'updateSecurityProfile' // Update a profile (propagates to referencing routes)
'deleteSecurityProfile' // Delete a profile (with optional force)
'getSecurityProfileUsage' // Get routes referencing a profile
// Network Targets
'getNetworkTargets' // List all network targets
'getNetworkTarget' // Get a single target by ID
'createNetworkTarget' // Create a reusable host:port target
'updateNetworkTarget' // Update a target (propagates to referencing routes)
'deleteNetworkTarget' // Delete a target (with optional force)
'getNetworkTargetUsage' // Get routes referencing a target
```
## API Client
@@ -1518,12 +1531,12 @@ const router = new DcRouter(options: IDcRouterOptions);
| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager |
| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager |
| `vpnManager` | `VpnManager` | VPN server lifecycle and client CRUD manager |
| `storageManager` | `StorageManager` | Storage backend |
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
| `metricsManager` | `MetricsManager` | Metrics collector |
| `cacheDb` | `CacheDb` | Cache database instance |
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
| `dcRouterDb` | `DcRouterDb` | Unified database instance (smartdata + smartdb) |
| `routeConfigManager` | `RouteConfigManager` | Programmatic route CRUD manager |
| `apiTokenManager` | `ApiTokenManager` | API token management |
| `referenceResolver` | `ReferenceResolver` | Security profile and network target resolver |
### Re-exported Types
@@ -1589,7 +1602,8 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 8 |
| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
| `test.reference-resolver.ts` | Security profiles, network targets, route resolution | 20 |
| `test.security-profiles-api.ts` | Profile/target API endpoints, auth enforcement | 13 |
## Docker / OCI Container Deployment

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.5',
version: '12.5.2',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -90,6 +90,13 @@ interface IIdentity {
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
#### Security & Reference Interfaces
| Interface | Description |
|-----------|-------------|
| `ISecurityProfile` | Reusable security config: id, name, description, security (ipAllowList, ipBlockList, maxConnections, rateLimit, etc.), extendsProfiles |
| `INetworkTarget` | Reusable host:port destination: id, name, description, host (string or string[]), port |
| `IRouteMetadata` | Route-to-reference links: securityProfileRef, networkTargetRef, snapshot names, lastResolvedAt |
#### Remote Ingress Interfaces
| Interface | Description |
|-----------|-------------|
@@ -241,6 +248,26 @@ interface ICertificateInfo {
| `IReq_GetRadiusStatistics` | `getRadiusStatistics` | RADIUS stats |
| `IReq_GetRadiusAccountingSummary` | `getRadiusAccountingSummary` | Accounting summary |
#### 🛡️ Security Profiles
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetSecurityProfiles` | `getSecurityProfiles` | List all security profiles |
| `IReq_GetSecurityProfile` | `getSecurityProfile` | Get a single profile by ID |
| `IReq_CreateSecurityProfile` | `createSecurityProfile` | Create a reusable security profile |
| `IReq_UpdateSecurityProfile` | `updateSecurityProfile` | Update a profile (propagates to routes) |
| `IReq_DeleteSecurityProfile` | `deleteSecurityProfile` | Delete a profile (with optional force) |
| `IReq_GetSecurityProfileUsage` | `getSecurityProfileUsage` | Get routes referencing a profile |
#### 🎯 Network Targets
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetNetworkTargets` | `getNetworkTargets` | List all network targets |
| `IReq_GetNetworkTarget` | `getNetworkTarget` | Get a single target by ID |
| `IReq_CreateNetworkTarget` | `createNetworkTarget` | Create a reusable host:port target |
| `IReq_UpdateNetworkTarget` | `updateNetworkTarget` | Update a target (propagates to routes) |
| `IReq_DeleteNetworkTarget` | `deleteNetworkTarget` | Delete a target (with optional force) |
| `IReq_GetNetworkTargetUsage` | `getNetworkTargetUsage` | Get routes referencing a target |
## Example: Full API Integration
> 💡 **Tip:** For a higher-level, object-oriented API, use [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) which wraps these interfaces with resource classes and builder patterns.

View File

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

View File

@@ -1441,6 +1441,37 @@ export const createRouteAction = routeManagementStatePart.createAction<{
}
});
export const updateRouteAction = routeManagementStatePart.createAction<{
id: string;
route?: any;
enabled?: boolean;
metadata?: any;
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_UpdateRoute
>('/typedrequest', 'updateRoute');
await request.fire({
identity: context.identity!,
id: dataArg.id,
route: dataArg.route,
enabled: dataArg.enabled,
metadata: dataArg.metadata,
});
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to update route',
};
}
});
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();

View File

@@ -2,7 +2,6 @@ import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { appRouter } from '../router.js';
import {
DeesElement,
css,
@@ -43,6 +42,12 @@ export class OpsDashboard extends DeesElement {
theme: 'light',
};
@state() accessor configState: appstate.IConfigState = {
config: null,
isLoading: false,
error: null,
};
// Store viewTabs as a property to maintain object references
private viewTabs = [
{
@@ -112,6 +117,20 @@ export class OpsDashboard extends DeesElement {
},
];
private get globalMessages() {
const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
const config = this.configState.config;
if (config && !config.cache.enabled) {
messages.push({
id: 'db-disabled',
type: 'warning',
message: 'Database is disabled. Creating and editing routes, profiles, targets, and API tokens is not available.',
dismissible: false,
});
}
return messages;
}
/**
* Get the current view tab based on the UI state's activeView.
* Used to pass the correct selectedView to dees-simple-appdash on initial render.
@@ -137,6 +156,14 @@ export class OpsDashboard extends DeesElement {
});
this.rxSubscriptions.push(loginSubscription);
// Subscribe to config state (for global warnings)
const configSubscription = appstate.configStatePart
.select((stateArg) => stateArg)
.subscribe((configState) => {
this.configState = configState;
});
this.rxSubscriptions.push(configSubscription);
// Subscribe to UI state
const uiSubscription = appstate.uiStatePart
.select((stateArg) => stateArg)
@@ -205,6 +232,7 @@ export class OpsDashboard extends DeesElement {
name="DCRouter OpsServer"
.viewTabs=${this.viewTabs}
.selectedView=${this.currentViewTab}
.globalMessages=${this.globalMessages}
>
</dees-simple-appdash>
</dees-simple-login>

View File

@@ -28,6 +28,13 @@ interface INetworkRequest {
@customElement('ops-view-network')
export class OpsViewNetwork extends DeesElement {
/** How far back the traffic chart shows */
private static readonly CHART_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
/** How often a new data point is added */
private static readonly UPDATE_INTERVAL_MS = 1000; // 1 second
/** Derived: max data points the buffer holds */
private static readonly MAX_DATA_POINTS = OpsViewNetwork.CHART_WINDOW_MS / OpsViewNetwork.UPDATE_INTERVAL_MS;
@state()
accessor statsState = appstate.statsStatePart.getState()!;
@@ -46,7 +53,7 @@ export class OpsViewNetwork extends DeesElement {
// Track if we need to update the chart to avoid unnecessary re-renders
private lastChartUpdate = 0;
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
private chartUpdateThreshold = OpsViewNetwork.UPDATE_INTERVAL_MS; // Minimum ms between chart updates
private trafficUpdateTimer: any = null;
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
@@ -104,13 +111,11 @@ export class OpsViewNetwork extends DeesElement {
private initializeTrafficData() {
const now = Date.now();
// Fixed 5 minute time range
const range = 5 * 60 * 1000; // 5 minutes
const bucketSize = range / 60; // 60 data points
const { MAX_DATA_POINTS, UPDATE_INTERVAL_MS } = OpsViewNetwork;
// Initialize with empty data points for both in and out
const emptyData = Array.from({ length: 60 }, (_, i) => {
const time = now - ((59 - i) * bucketSize);
const emptyData = Array.from({ length: MAX_DATA_POINTS }, (_, i) => {
const time = now - ((MAX_DATA_POINTS - 1 - i) * UPDATE_INTERVAL_MS);
return {
x: new Date(time).toISOString(),
y: 0,
@@ -143,23 +148,23 @@ export class OpsViewNetwork extends DeesElement {
y: Math.round((p.out * 8) / 1000000 * 10) / 10,
}));
// Use history as the chart data, keeping the most recent 60 points (5 min window)
const sliceStart = Math.max(0, historyIn.length - 60);
const { MAX_DATA_POINTS, UPDATE_INTERVAL_MS } = OpsViewNetwork;
// Use history as the chart data, keeping the most recent points within the window
const sliceStart = Math.max(0, historyIn.length - MAX_DATA_POINTS);
this.trafficDataIn = historyIn.slice(sliceStart);
this.trafficDataOut = historyOut.slice(sliceStart);
// If fewer than 60 points, pad the front with zeros
if (this.trafficDataIn.length < 60) {
// If fewer than MAX_DATA_POINTS, pad the front with zeros
if (this.trafficDataIn.length < MAX_DATA_POINTS) {
const now = Date.now();
const range = 5 * 60 * 1000;
const bucketSize = range / 60;
const padCount = 60 - this.trafficDataIn.length;
const padCount = MAX_DATA_POINTS - this.trafficDataIn.length;
const firstTimestamp = this.trafficDataIn.length > 0
? new Date(this.trafficDataIn[0].x).getTime()
: now;
const padIn = Array.from({ length: padCount }, (_, i) => ({
x: new Date(firstTimestamp - ((padCount - i) * bucketSize)).toISOString(),
x: new Date(firstTimestamp - ((padCount - i) * UPDATE_INTERVAL_MS)).toISOString(),
y: 0,
}));
const padOut = padIn.map(p => ({ ...p }));
@@ -287,27 +292,17 @@ export class OpsViewNetwork extends DeesElement {
{
name: 'Inbound',
data: this.trafficDataIn,
color: '#22c55e', // Green for download
color: '#22c55e',
},
{
name: 'Outbound',
data: this.trafficDataOut,
color: '#8b5cf6', // Purple for upload
color: '#8b5cf6',
}
]}
.stacked=${false}
.realtimeMode=${true}
.rollingWindow=${OpsViewNetwork.CHART_WINDOW_MS}
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
.tooltipFormatter=${(point: any) => {
const mbps = point.y || 0;
const seriesName = point.series?.name || 'Throughput';
const timestamp = new Date(point.x).toLocaleTimeString();
return `
<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">${timestamp}</div>
<div>${seriesName}: ${mbps.toFixed(2)} Mbit/s</div>
</div>
`;
}}
></dees-chart-area>
<!-- Top IPs Section -->
@@ -719,9 +714,8 @@ export class OpsViewNetwork extends DeesElement {
private startTrafficUpdateTimer() {
this.stopTrafficUpdateTimer(); // Clear any existing timer
this.trafficUpdateTimer = setInterval(() => {
// Add a new data point every second
this.addTrafficDataPoint();
}, 1000); // Update every second
}, OpsViewNetwork.UPDATE_INTERVAL_MS);
}
private addTrafficDataPoint() {
@@ -752,7 +746,7 @@ export class OpsViewNetwork extends DeesElement {
};
// In-place mutation then reassign for Lit reactivity (avoids 4 intermediate arrays)
if (this.trafficDataIn.length >= 60) {
if (this.trafficDataIn.length >= OpsViewNetwork.MAX_DATA_POINTS) {
this.trafficDataIn.shift();
this.trafficDataOut.shift();
}

View File

@@ -64,6 +64,7 @@ export class OpsViewNetworkTargets extends DeesElement {
];
return html`
<ops-sectionheading>Network Targets</ops-sectionheading>
<div class="targetsContainer">
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
<dees-table
@@ -81,8 +82,8 @@ export class OpsViewNetworkTargets extends DeesElement {
name: 'Create Target',
iconName: 'lucide:plus',
type: ['header' as const],
actionFunc: async (_: any, table: any) => {
await this.showCreateTargetDialog(table);
actionFunc: async () => {
await this.showCreateTargetDialog();
},
},
{
@@ -96,16 +97,18 @@ export class OpsViewNetworkTargets extends DeesElement {
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['contextmenu' as const],
actionFunc: 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],
actionFunc: 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);
},
},
@@ -115,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',
@@ -128,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, {
@@ -143,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');
@@ -163,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, {
@@ -179,7 +185,6 @@ export class OpsViewNetworkTargets extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}

View File

@@ -121,11 +121,15 @@ export class OpsViewOverview extends DeesElement {
<dees-chart-area
.label=${'Email Traffic (24h)'}
.series=${this.getEmailTrafficSeries()}
.realtimeMode=${true}
.rollingWindow=${86400000}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-area
.label=${'DNS Queries (24h)'}
.series=${this.getDnsQuerySeries()}
.realtimeMode=${true}
.rollingWindow=${86400000}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-log

View File

@@ -204,7 +204,10 @@ export class OpsViewRoutes extends DeesElement {
? html`
<sz-route-list-view
.routes=${szRoutes}
.showActionsFilter=${(route: any) => route.tags?.includes('programmatic') ?? false}
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
></sz-route-list-view>
`
: html`
@@ -337,6 +340,165 @@ export class OpsViewRoutes extends DeesElement {
}
}
private async handleRouteEdit(e: CustomEvent) {
const clickedRoute = e.detail;
if (!clickedRoute) return;
const merged = this.routeState.mergedRoutes.find(
(mr) => mr.route.name === clickedRoute.name,
);
if (!merged || !merged.storedRouteId) return;
this.showEditRouteDialog(merged);
}
private async handleRouteDelete(e: CustomEvent) {
const clickedRoute = e.detail;
if (!clickedRoute) return;
const merged = this.routeState.mergedRoutes.find(
(mr) => mr.route.name === clickedRoute.name,
);
if (!merged || !merged.storedRouteId) return;
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: `Delete Route: ${merged.route.name}`,
content: html`
<div style="color: #ccc; padding: 8px 0;">
<p>Are you sure you want to delete this route? This action cannot be undone.</p>
</div>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Delete',
iconName: 'lucide:trash-2',
action: async (modalArg: any) => {
await appstate.routeManagementStatePart.dispatchAction(
appstate.deleteRouteAction,
merged.storedRouteId!,
);
await modalArg.destroy();
},
},
],
});
}
private async showEditRouteDialog(merged: interfaces.data.IMergedRoute) {
const { DeesModal } = await import('@design.estate/dees-catalog');
const profiles = this.profilesTargetsState.profiles;
const targets = this.profilesTargetsState.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})`,
})),
];
const route = merged.route;
const currentPorts = Array.isArray(route.match.ports)
? route.match.ports.map((p: any) => typeof p === 'number' ? String(p) : `${p.from}-${p.to}`).join(', ')
: String(route.match.ports);
const currentDomains: string[] = route.match.domains
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
: [];
const firstTarget = route.action.targets?.[0];
const currentTargetHost = firstTarget
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
: '';
const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : '';
await DeesModal.createAndShow({
heading: `Edit Route: ${route.name}`,
content: html`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Route Name'} .value=${route.name || ''} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .value=${currentPorts} .required=${true}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${merged.metadata?.securityProfileRef || ''}></dees-input-dropdown>
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${merged.metadata?.networkTargetRef || ''}></dees-input-dropdown>
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${currentTargetHost}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></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();
if (!formData.name || !formData.ports) return;
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
const domains: string[] = Array.isArray(formData.domains)
? formData.domains.filter(Boolean)
: [];
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
const updatedRoute: any = {
name: formData.name,
match: {
ports,
...(domains.length > 0 ? { domains } : {}),
},
action: {
type: 'forward',
targets: [
{
host: formData.targetHost || 'localhost',
port: parseInt(formData.targetPort, 10) || 443,
},
],
},
...(priority != null && !isNaN(priority) ? { priority } : {}),
};
const metadata: any = {};
if (formData.securityProfileRef) {
metadata.securityProfileRef = formData.securityProfileRef;
}
if (formData.networkTargetRef) {
metadata.networkTargetRef = formData.networkTargetRef;
}
await appstate.routeManagementStatePart.dispatchAction(
appstate.updateRouteAction,
{
id: merged.storedRouteId!,
route: updatedRoute,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
},
);
await modalArg.destroy();
},
},
],
});
}
private async showCreateRouteDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
const profiles = this.profilesTargetsState.profiles;
@@ -364,7 +526,8 @@ export class OpsViewRoutes extends DeesElement {
<dees-form>
<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-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'}></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>
@@ -387,15 +550,16 @@ export class OpsViewRoutes extends DeesElement {
if (!formData.name || !formData.ports) return;
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
const domains = formData.domains
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
: undefined;
const domains: string[] = Array.isArray(formData.domains)
? formData.domains.filter(Boolean)
: [];
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
const route: any = {
name: formData.name,
match: {
ports,
...(domains && domains.length > 0 ? { domains } : {}),
...(domains.length > 0 ? { domains } : {}),
},
action: {
type: 'forward',
@@ -406,6 +570,7 @@ export class OpsViewRoutes extends DeesElement {
},
],
},
...(priority != null && !isNaN(priority) ? { priority } : {}),
};
// Build metadata if profile/target selected

View File

@@ -64,6 +64,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
];
return html`
<ops-sectionheading>Security Profiles</ops-sectionheading>
<div class="profilesContainer">
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
<dees-table
@@ -89,8 +90,8 @@ export class OpsViewSecurityProfiles extends DeesElement {
name: 'Create Profile',
iconName: 'lucide:plus',
type: ['header' as const],
actionFunc: async (_: any, table: any) => {
await this.showCreateProfileDialog(table);
actionFunc: async () => {
await this.showCreateProfileDialog();
},
},
{
@@ -104,16 +105,18 @@ export class OpsViewSecurityProfiles extends DeesElement {
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['contextmenu' as const],
actionFunc: 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],
actionFunc: 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);
},
},
@@ -123,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',
@@ -137,10 +140,12 @@ export class OpsViewSecurityProfiles 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();
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
@@ -158,12 +163,11 @@ export class OpsViewSecurityProfiles extends DeesElement {
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}`,
@@ -177,10 +181,12 @@ export class OpsViewSecurityProfiles 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();
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
@@ -199,7 +205,6 @@ export class OpsViewSecurityProfiles extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}

View File

@@ -68,6 +68,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- API token creation, revocation, and scope management
- Routes tab and API Tokens tab in unified view
### 🛡️ Security Profiles & Network Targets
- Create, edit, and delete reusable security profiles (IP allow/block lists, rate limits, max connections)
- Create, edit, and delete reusable network targets (host:port destinations)
- In-row and context menu actions for quick editing
- Changes propagate automatically to all referencing routes
### ⚙️ Configuration
- Read-only display of current system configuration
- Status badges for boolean values (enabled/disabled)