feat(routes,email): persist system DNS routes with runtime hydration and add reusable email ops DNS helpers

This commit is contained in:
2026-04-15 19:59:04 +00:00
parent e0386beb15
commit 39f449cbe4
24 changed files with 1221 additions and 2525 deletions

View File

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

View File

@@ -2150,7 +2150,7 @@ export const updateRouteAction = routeManagementStatePart.createAction<{
interfaces.requests.IReq_UpdateRoute
>('/typedrequest', 'updateRoute');
await request.fire({
const response = await request.fire({
identity: context.identity!,
id: dataArg.id,
route: dataArg.route,
@@ -2158,6 +2158,10 @@ export const updateRouteAction = routeManagementStatePart.createAction<{
metadata: dataArg.metadata,
});
if (!response.success) {
throw new Error(response.message || 'Failed to update route');
}
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
@@ -2177,11 +2181,15 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
interfaces.requests.IReq_DeleteRoute
>('/typedrequest', 'deleteRoute');
await request.fire({
const response = await request.fire({
identity: context.identity!,
id: routeId,
});
if (!response.success) {
throw new Error(response.message || 'Failed to delete route');
}
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
@@ -2204,12 +2212,16 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
interfaces.requests.IReq_ToggleRoute
>('/typedrequest', 'toggleRoute');
await request.fire({
const response = await request.fire({
identity: context.identity!,
id: dataArg.id,
enabled: dataArg.enabled,
});
if (!response.success) {
throw new Error(response.message || 'Failed to toggle route');
}
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
@@ -2765,4 +2777,4 @@ startAutoRefresh();
// Connect TypedSocket if already logged in (e.g., persistent session)
if (loginStatePart.getState()!.isLoggedIn) {
connectSocket();
}
}

View File

@@ -272,15 +272,13 @@ export class OpsViewRoutes extends DeesElement {
const clickedRoute = e.detail;
if (!clickedRoute) return;
// Find the corresponding merged route
const merged = this.routeState.mergedRoutes.find(
(mr) => mr.route.name === clickedRoute.name,
);
const merged = this.findMergedRoute(clickedRoute);
if (!merged) return;
const { DeesModal } = await import('@design.estate/dees-catalog');
const meta = merged.metadata;
const isSystemManaged = this.isSystemManagedRoute(merged);
await DeesModal.createAndShow({
heading: `Route: ${merged.route.name}`,
content: html`
@@ -288,6 +286,7 @@ export class OpsViewRoutes extends DeesElement {
<p>Origin: <strong style="color: #0af;">${merged.origin}</strong></p>
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
<p>ID: <code style="color: #888;">${merged.id}</code></p>
${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
${meta?.sourceProfileName ? html`<p>Source Profile: <strong style="color: #a78bfa;">${meta.sourceProfileName}</strong></p>` : ''}
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
</div>
@@ -304,25 +303,29 @@ export class OpsViewRoutes extends DeesElement {
await modalArg.destroy();
},
},
{
name: 'Edit',
iconName: 'lucide:pencil',
action: async (modalArg: any) => {
await modalArg.destroy();
this.showEditRouteDialog(merged);
},
},
{
name: 'Delete',
iconName: 'lucide:trash-2',
action: async (modalArg: any) => {
await appstate.routeManagementStatePart.dispatchAction(
appstate.deleteRouteAction,
merged.id,
);
await modalArg.destroy();
},
},
...(!isSystemManaged
? [
{
name: 'Edit',
iconName: 'lucide:pencil',
action: async (modalArg: any) => {
await modalArg.destroy();
this.showEditRouteDialog(merged);
},
},
{
name: 'Delete',
iconName: 'lucide:trash-2',
action: async (modalArg: any) => {
await appstate.routeManagementStatePart.dispatchAction(
appstate.deleteRouteAction,
merged.id,
);
await modalArg.destroy();
},
},
]
: []),
{
name: 'Close',
iconName: 'lucide:x',
@@ -336,10 +339,9 @@ export class OpsViewRoutes extends DeesElement {
const clickedRoute = e.detail;
if (!clickedRoute) return;
const merged = this.routeState.mergedRoutes.find(
(mr) => mr.route.name === clickedRoute.name,
);
const merged = this.findMergedRoute(clickedRoute);
if (!merged) return;
if (this.isSystemManagedRoute(merged)) return;
this.showEditRouteDialog(merged);
}
@@ -348,10 +350,9 @@ export class OpsViewRoutes extends DeesElement {
const clickedRoute = e.detail;
if (!clickedRoute) return;
const merged = this.routeState.mergedRoutes.find(
(mr) => mr.route.name === clickedRoute.name,
);
const merged = this.findMergedRoute(clickedRoute);
if (!merged) return;
if (this.isSystemManagedRoute(merged)) return;
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
@@ -675,6 +676,23 @@ export class OpsViewRoutes extends DeesElement {
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
}
private findMergedRoute(clickedRoute: { id?: string; name?: string }): interfaces.data.IMergedRoute | undefined {
if (clickedRoute.id) {
const routeById = this.routeState.mergedRoutes.find((mr) => mr.id === clickedRoute.id);
if (routeById) return routeById;
}
if (clickedRoute.name) {
return this.routeState.mergedRoutes.find((mr) => mr.route.name === clickedRoute.name);
}
return undefined;
}
private isSystemManagedRoute(merged: interfaces.data.IMergedRoute): boolean {
return merged.origin !== 'api';
}
async firstUpdated() {
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);

View File

@@ -1,273 +1,72 @@
# @serve.zone/dcrouter-web
Web-based Operations Dashboard for DcRouter. 🖥️
Browser UI package for dcrouter's operations dashboard. 🖥️
A modern, reactive web application for monitoring and managing your DcRouter instance in real-time. Built with web components using [@design.estate/dees-element](https://code.foss.global/design.estate/dees-element) and [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog).
This package contains the browser entrypoint, app state, router, and web components that power the Ops dashboard served by dcrouter.
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Features
## What Is In Here
### 🔐 Secure Authentication
- JWT-based login with persistent sessions (IndexedDB)
- Automatic session expiry detection and cleanup
- Secure username/password authentication
| Path | Purpose |
| --- | --- |
| `index.ts` | Browser entrypoint that initializes routing and renders `<ops-dashboard>` |
| `appstate.ts` | Central reactive state and action definitions |
| `router.ts` | URL-based dashboard routing |
| `elements/` | Dashboard views and reusable UI pieces |
### 📊 Overview Dashboard
- Real-time server statistics (CPU, memory, uptime)
- Active connection counts and email throughput
- DNS query metrics and RADIUS session tracking
- Auto-refreshing with configurable intervals
## Main Views
### 🌐 Network View
- Active connection monitoring with real-time data from SmartProxy
- Top connected IPs with connection counts and percentages
- Throughput rates (inbound/outbound in kbit/s, Mbit/s, Gbit/s)
- Traffic chart with selectable time ranges
The dashboard currently includes views for:
### 📧 Email Management
- **Queued** — Emails pending delivery with queue position
- **Sent** — Successfully delivered emails with timestamps
- **Failed** — Failed emails with resend capability
- **Security** — Security incidents from email processing
- Bounce record management and suppression list controls
- overview and configuration
- network activity and route management
- source profiles, target profiles, and network targets
- email activity and email domains
- DNS providers, domains, DNS records, and certificates
- API tokens and users
- VPN, remote ingress, logs, and security views
### 🔐 Certificate Management
- Domain-centric certificate overview with status indicators
- Certificate source tracking (ACME, provision function, static)
- Expiry date monitoring and alerts
- Per-domain backoff status for failed provisions
- One-click reprovisioning per domain
- Certificate import, export, and deletion
## Route Management UX
### 🌍 Remote Ingress Management
- Edge node registration with name, ports, and tags
- Real-time connection status (connected/disconnected/disabled)
- Public IP and active tunnel count per edge
- Auto-derived port display with manual/derived breakdown
- **Connection token generation** — one-click "Copy Token" for easy edge provisioning
- Enable/disable, edit, secret regeneration, and delete actions
The web UI reflects dcrouter's current route ownership model:
### 🔐 VPN Management
- VPN server status with forwarding mode, subnet, and WireGuard port
- Client registration table with create, enable/disable, and delete actions
- WireGuard config download, clipboard copy, and **QR code display** on client creation
- QR code export for existing clients — scan with WireGuard mobile app (iOS/Android)
- Per-client telemetry (bytes sent/received, keepalives)
- Server public key display for manual client configuration
- system routes are shown separately from user routes
- system routes are visible and toggleable
- system routes are not directly editable or deletable
- API routes are fully managed through the route-management forms
### 📜 Log Viewer
- Real-time log streaming
- Filter by log level (error, warning, info, debug)
- Search and time-range selection
## How It Talks To dcrouter
### 🛣️ Route & API Token Management
- Programmatic route CRUD with enable/disable and override controls
- API token creation, revocation, and scope management
- Routes tab and API Tokens tab in unified view
The frontend uses TypedRequest and shared interfaces from `@serve.zone/dcrouter-interfaces`.
### 🛡️ 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
State actions in `appstate.ts` fetch and mutate:
### ⚙️ Configuration
- Read-only display of current system configuration
- Status badges for boolean values (enabled/disabled)
- Array values displayed as pills with counts
- Section icons and formatted byte/time values
- stats and health
- logs
- routes and tokens
- certificates and ACME config
- DNS providers, domains, and records
- email domains and email operations
- VPN, remote ingress, and RADIUS data
### 🛡️ Security Dashboard
- IP reputation monitoring
- Rate limit status across domains
- Blocked connection tracking
- Security event timeline
## Development Notes
## Architecture
### Technology Stack
| Layer | Package | Purpose |
|-------|---------|---------|
| **Components** | `@design.estate/dees-element` | Web component framework (lit-element based) |
| **UI Kit** | `@design.estate/dees-catalog` | Pre-built components (tables, charts, forms, app shell) |
| **State** | `@push.rocks/smartstate` | Reactive state management with persistent/soft modes |
| **Routing** | Client-side router | URL-synchronized view navigation |
| **API** | `@api.global/typedrequest` | Type-safe communication with OpsServer |
| **Types** | `@serve.zone/dcrouter-interfaces` | Shared TypedRequest interface definitions |
### Component Structure
```
ts_web/
├── index.ts # Entry point — renders <ops-dashboard>
├── appstate.ts # State management (all state parts + actions)
├── router.ts # Client-side routing (AppRouter)
├── plugins.ts # Dependency imports
└── elements/
├── ops-dashboard.ts # Main app shell
├── ops-view-overview.ts # Overview statistics
├── ops-view-network.ts # Network monitoring
├── ops-view-emails.ts # Email queue management
├── ops-view-certificates.ts # Certificate overview & reprovisioning
├── ops-view-remoteingress.ts # Remote ingress edge management
├── ops-view-vpn.ts # VPN client management
├── ops-view-logs.ts # Log viewer
├── ops-view-routes.ts # Route & API token management
├── ops-view-config.ts # Configuration display
├── ops-view-security.ts # Security dashboard
└── shared/
├── css.ts # Shared styles
└── ops-sectionheading.ts # Section heading component
```
### State Management
The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled actions with `autoPause: 'visibility'`, and batched updates:
| State Part | Mode | Description |
|-----------|------|-------------|
| `loginStatePart` | Persistent (IndexedDB) | JWT identity and login status |
| `statsStatePart` | Soft (memory) | Server, email, DNS, security, RADIUS, VPN metrics |
| `configStatePart` | Soft | Current system configuration |
| `uiStatePart` | Soft | Active view, sidebar, auto-refresh, theme |
| `logStatePart` | Soft | Recent logs, streaming status, filters |
| `networkStatePart` | Soft | Connections, IPs, throughput rates |
| `emailOpsStatePart` | Soft | Email queues, bounces, suppression list |
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
| `vpnStatePart` | Soft | VPN clients, server status, new client config |
### Tab Visibility Optimization
The dashboard automatically pauses all background activity when the browser tab is hidden and resumes when visible:
- **Auto-refresh polling** uses `createScheduledAction` with `autoPause: 'visibility'` — stops HTTP requests while the tab is sleeping
- **In-flight guard** prevents concurrent refresh requests from piling up
- **WebSocket connection** disconnects when hidden and reconnects when visible, preventing log entry accumulation
- **Network traffic timer** pauses chart updates when the tab is backgrounded
- **Log entry batching** — incoming WebSocket log pushes are buffered and flushed once per animation frame to avoid per-entry re-renders
### Actions
```typescript
// Authentication
loginAction(username, password) // JWT login
logoutAction() // Clear session
// Data fetching (auto-refresh compatible)
fetchAllStatsAction() // Server + email + DNS + security stats
fetchConfigurationAction() // System configuration
fetchRecentLogsAction() // Log entries
fetchNetworkStatsAction() // Connection + throughput data
// Email operations
fetchQueuedEmailsAction() // Pending emails
fetchSentEmailsAction() // Delivered emails
fetchFailedEmailsAction() // Failed emails
fetchSecurityIncidentsAction() // Security events
fetchBounceRecordsAction() // Bounce records
resendEmailAction(emailId) // Re-queue failed email
removeFromSuppressionAction(email) // Remove from suppression list
// Certificates
fetchCertificateOverviewAction() // All certificates with summary
reprovisionCertificateAction(domain) // Reprovision a certificate
deleteCertificateAction(domain) // Delete a certificate
importCertificateAction(cert) // Import a certificate
fetchCertificateExport(domain) // Export (standalone function)
// Remote Ingress
fetchRemoteIngressAction() // Edges + statuses
createRemoteIngressAction(data) // Create new edge
updateRemoteIngressAction(data) // Update edge settings
deleteRemoteIngressAction(id) // Remove edge
regenerateRemoteIngressSecretAction(id) // New secret
toggleRemoteIngressAction(id, enabled) // Enable/disable
clearNewEdgeSecretAction() // Dismiss secret banner
fetchConnectionToken(edgeId) // Get connection token (standalone function)
// VPN
fetchVpnAction() // Clients + server status
createVpnClientAction(data) // Create new VPN client
deleteVpnClientAction(clientId) // Remove VPN client
toggleVpnClientAction(id, enabled) // Enable/disable
clearNewClientConfigAction() // Dismiss config banner
```
### Client-Side Routing
```
/overview → Overview dashboard
/network → Network monitoring
/emails → Email management
/emails/queued → Queued emails
/emails/sent → Sent emails
/emails/failed → Failed emails
/emails/security → Security incidents
/certificates → Certificate management
/remoteingress → Remote ingress edge management
/vpn → VPN client management
/routes → Route & API token management
/logs → Log viewer
/configuration → System configuration
/security → Security dashboard
```
URL state is synchronized with the UI — bookmarking and deep linking fully supported.
## Development
### Running Locally
Start DcRouter with OpsServer enabled:
```typescript
import { DcRouter } from '@serve.zone/dcrouter';
const router = new DcRouter({
// OpsServer starts automatically on port 3000
smartProxyConfig: { routes: [/* your routes */] }
});
await router.start();
// Dashboard at http://localhost:3000
```
### Building
The browser bundle is built from this package and served by the main dcrouter package.
```bash
# Build the bundle
pnpm run bundle
# Watch for development (auto-rebuild + restart)
pnpm run watch
```
The bundle is output to `./dist_serve/bundle.js` and served by the OpsServer.
The generated bundle is written into `dist_serve/` by the main build pipeline.
### Adding a New View
## When To Use This Package
1. Create a view component in `elements/`:
```typescript
import { DeesElement, customElement, html, css } from '@design.estate/dees-element';
@customElement('ops-view-myview')
export class OpsViewMyView extends DeesElement {
public static styles = [css`:host { display: block; padding: 24px; }`];
public render() {
return html`<ops-sectionheading>My View</ops-sectionheading>`;
}
}
```
2. Add it to the dashboard tabs in `ops-dashboard.ts`
3. Add the route in `router.ts`
4. Add any state management in `appstate.ts`
- Use it if you want the dashboard frontend as a package/module boundary.
- Use the main `@serve.zone/dcrouter` package if you want the server that actually serves this UI.
## License and Legal Information