feat(remote-ingress): add Remote Ingress hub integration, OpsServer UI, APIs, and docs

This commit is contained in:
2026-02-18 18:47:18 +00:00
parent 86e6c4f600
commit dce1de8c4b
13 changed files with 456 additions and 43 deletions

View File

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

View File

@@ -200,7 +200,7 @@ export interface IRemoteIngressState {
edges: interfaces.data.IRemoteIngress[];
statuses: interfaces.data.IRemoteIngressStatus[];
selectedEdgeId: string | null;
newEdgeSecret: string | null;
newEdgeId: string | null;
isLoading: boolean;
error: string | null;
lastUpdated: number;
@@ -212,7 +212,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
edges: [],
statuses: [],
selectedEdgeId: null,
newEdgeSecret: null,
newEdgeId: null,
isLoading: false,
error: null,
lastUpdated: 0,
@@ -928,11 +928,12 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
});
if (response.success) {
// Refresh the list and store the new secret for display
// Refresh the list
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
return {
...statePartArg.getState(),
newEdgeSecret: response.edge.secret,
newEdgeId: response.edge.id,
};
}
@@ -1023,7 +1024,7 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
if (response.success) {
return {
...currentState,
newEdgeSecret: response.secret,
newEdgeId: edgeId,
};
}
@@ -1037,11 +1038,11 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
}
);
export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
async (statePartArg) => {
return {
...statePartArg.getState(),
newEdgeSecret: null,
newEdgeId: null,
};
}
);

View File

@@ -382,7 +382,7 @@ export class OpsViewCertificates extends DeesElement {
},
{
name: 'View Details',
iconName: 'lucide:Search',
iconName: 'fa:magnifyingGlass',
type: ['doubleClick', 'contextmenu'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item;

View File

@@ -287,7 +287,7 @@ export class OpsViewNetwork extends DeesElement {
.dataActions=${[
{
name: 'View Details',
iconName: 'lucide:Search',
iconName: 'fa:magnifyingGlass',
type: ['inRow', 'doubleClick', 'contextmenu'],
actionFunc: async (actionData) => {
await this.showRequestDetails(actionData.item);
@@ -435,7 +435,7 @@ export class OpsViewNetwork extends DeesElement {
actions: [
{
name: 'View Details',
iconName: 'lucide:Search',
iconName: 'fa:magnifyingGlass',
action: async () => {
},
},

View File

@@ -176,13 +176,39 @@ export class OpsViewRemoteIngress extends DeesElement {
return html`
<ops-sectionheading>Remote Ingress</ops-sectionheading>
${this.riState.newEdgeSecret ? html`
${this.riState.newEdgeId ? html`
<div class="secretDialog">
<strong>Edge Secret (copy now - shown only once):</strong>
<code>${this.riState.newEdgeSecret}</code>
<div class="warning">This secret will not be shown again. Save it securely.</div>
<strong>Edge created successfully!</strong>
<div class="warning">Copy the connection token now. Use it with edge.start({ token: '...' }).</div>
<dees-button
@click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeSecretAction, null)}
@click=${async () => {
const { DeesToast } = await import('@design.estate/dees-catalog');
try {
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId);
if (response.success && response.token) {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(response.token);
} else {
const textarea = document.createElement('textarea');
textarea.value = response.token;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
DeesToast.show({ message: 'Connection token copied!', type: 'success', duration: 3000 });
} else {
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
}
}}
>Copy Connection Token</dees-button>
<dees-button
@click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeIdAction, null)}
>Dismiss</dees-button>
</div>
` : ''}
@@ -348,7 +374,7 @@ export class OpsViewRemoteIngress extends DeesElement {
},
{
name: 'Copy Token',
iconName: 'lucide:clipboard-copy',
iconName: 'lucide:ClipboardCopy',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
@@ -356,7 +382,19 @@ export class OpsViewRemoteIngress extends DeesElement {
try {
const response = await appstate.fetchConnectionToken(edge.id);
if (response.success && response.token) {
await navigator.clipboard.writeText(response.token);
// Use clipboard API with fallback for non-HTTPS contexts
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(response.token);
} else {
const textarea = document.createElement('textarea');
textarea.value = response.token;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
DeesToast.show({ message: `Connection token copied for ${edge.name}`, type: 'success', duration: 3000 });
} else {
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });

View File

@@ -40,6 +40,15 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- Expiry date monitoring and alerts
- Per-domain backoff status for failed provisions
- One-click reprovisioning per domain
- Certificate import, export, and deletion
### 🌍 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
### 📜 Log Viewer
- Real-time log streaming
@@ -85,6 +94,7 @@ ts_web/
├── 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-logs.ts # Log viewer
├── ops-view-config.ts # Configuration display
├── ops-view-security.ts # Security dashboard
@@ -106,6 +116,8 @@ The app uses `@push.rocks/smartstate` with multiple state parts:
| `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 |
### Actions
@@ -128,6 +140,23 @@ 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)
```
### Client-Side Routing
@@ -141,6 +170,7 @@ removeFromSuppressionAction(email) // Remove from suppression list
/emails/failed → Failed emails
/emails/security → Security incidents
/certificates → Certificate management
/remoteingress → Remote ingress edge management
/logs → Log viewer
/configuration → System configuration
/security → Security dashboard