Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 466654ee4c | |||
| f1a11e3f6a | |||
| e193b3a8eb | |||
| 1bbf31605c | |||
| f2cfa923a0 | |||
| cdc77305e5 | |||
| 835537f789 | |||
| 754b223f62 | |||
| 0a39d50d20 | |||
| de7b9f7ec5 | |||
| bd959464c7 | |||
| 36b629676f |
@@ -0,0 +1,17 @@
|
|||||||
|
# Agent Instructions for dcrouter
|
||||||
|
|
||||||
|
## Database & Migrations
|
||||||
|
|
||||||
|
### Collection Names
|
||||||
|
smartdata uses the **exact class name** as the MongoDB collection name. No lowercasing.
|
||||||
|
- `StoredRouteDoc` → collection `StoredRouteDoc`
|
||||||
|
- `TargetProfileDoc` → collection `TargetProfileDoc`
|
||||||
|
- `RouteDoc` → collection `RouteDoc`
|
||||||
|
|
||||||
|
When writing migrations in `ts_migrations/index.ts`, use the exact class name casing in `ctx.mongo!.collection('ClassName')` and `db.listCollections({ name: 'ClassName' })`.
|
||||||
|
|
||||||
|
### Migration Rules
|
||||||
|
- All DB schema migrations go EXCLUSIVELY in `ts_migrations/index.ts` as smartmigration steps.
|
||||||
|
- NEVER put migration logic in application code (services, managers, startup hooks).
|
||||||
|
- Migration step `.to()` version must match the release version so smartmigration can plan the step.
|
||||||
|
- Steps must be idempotent — smartmigration may re-run them in skip-forward resume mode.
|
||||||
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.17.3 - fix(ops-view-routes)
|
||||||
|
sync route filter toggle selection via component changeSubject
|
||||||
|
|
||||||
|
- Replaces the inline change handler on the route filter toggle with a subscription to the component's changeSubject in firstUpdated.
|
||||||
|
- Ensures switching between user and system routes updates the view reliably and is cleaned up through existing rxSubscriptions management.
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.17.2 - fix(monitoring)
|
||||||
|
exclude unconfigured routes from domain activity aggregation
|
||||||
|
|
||||||
|
- Removes fallback aggregation that reported routes without domain configuration as synthetic domain entries based on route names
|
||||||
|
- Keeps domain activity focused on configured domain mappings when splitting connection and throughput metrics
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.17.1 - fix(monitoring)
|
||||||
|
stop allocating route metrics to domains when no request data exists
|
||||||
|
|
||||||
|
- Removes the equal-split fallback for shared routes in MetricsManager.
|
||||||
|
- Sets the proportional share to zero when a route has no recorded requests, avoiding inflated per-domain connection and throughput totals.
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.17.0 - feat(monitoring,network-ui,routes)
|
||||||
|
add request-based domain activity metrics and split routes into user and system views
|
||||||
|
|
||||||
|
- Domain activity now includes per-domain request counts and distributes route throughput and connections using request-level metrics instead of equal route sharing.
|
||||||
|
- Network activity UI displays request counts and updates the domain activity description to reflect request-level aggregation.
|
||||||
|
- Routes UI adds a toggle to filter between user-created and system-generated routes, updates summary card labels, and adjusts empty states accordingly.
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.16.2 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^27.6.0
|
||||||
|
|
||||||
|
- updates @push.rocks/smartproxy from ^27.5.0 to ^27.6.0 in package.json
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.16.1 - fix(migrations)
|
||||||
|
use exact smartdata collection names in route unification migration
|
||||||
|
|
||||||
|
- Update the 13.16.0 migration to rename StoredRouteDoc to RouteDoc using case-sensitive collection names
|
||||||
|
- Apply the origin backfill against the RouteDoc collection and drop RouteOverrideDoc with matching class-name casing
|
||||||
|
- Clarify migration description and comments to reflect smartdata's exact class-name collection mapping
|
||||||
|
|
||||||
## 2026-04-13 - 13.16.0 - feat(routes)
|
## 2026-04-13 - 13.16.0 - feat(routes)
|
||||||
unify route storage and management across config, email, dns, and API origins
|
unify route storage and management across config, email, dns, and API origins
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "13.16.0",
|
"version": "13.17.3",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.5.2",
|
"@push.rocks/smartnetwork": "^4.5.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^27.5.0",
|
"@push.rocks/smartproxy": "^27.6.0",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
|
|||||||
Generated
+5
-5
@@ -81,8 +81,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^27.5.0
|
specifier: ^27.6.0
|
||||||
version: 27.5.0
|
version: 27.6.0
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -1287,8 +1287,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.5.0':
|
'@push.rocks/smartproxy@27.6.0':
|
||||||
resolution: {integrity: sha512-QIXrVQtAoqBCv+9ScLOdGcizN55svJuGCfMDsDaBVtwS3Tva30IxuEL3usNTHABveuI8slaWzSxTabmTULDOwA==}
|
resolution: {integrity: sha512-1mPzabUKhlC0EdeI7Hjee/aiptTsOLftbq8oWBTlIg9JhCQwkIs5UNGTJV/VvlEflJKnay8TbzLzlr95gUr/1w==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -6521,7 +6521,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.5.0':
|
'@push.rocks/smartproxy@27.6.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.16.0',
|
version: '13.17.3',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ export class MetricsManager {
|
|||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
backends: [] as Array<any>,
|
backends: [] as Array<any>,
|
||||||
domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number }>,
|
domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number }>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,11 +720,20 @@ export class MetricsManager {
|
|||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
||||||
|
|
||||||
// Build domain activity from per-route metrics
|
// Build domain activity using per-IP domain request counts from Rust engine
|
||||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||||
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
||||||
|
|
||||||
// Map route name → ALL its domains (not just the first one)
|
// Aggregate per-IP domain request counts into per-domain totals
|
||||||
|
const domainRequestTotals = new Map<string, number>();
|
||||||
|
const domainRequestsByIP = proxyMetrics.connections.domainRequestsByIP();
|
||||||
|
for (const [, domainMap] of domainRequestsByIP) {
|
||||||
|
for (const [domain, count] of domainMap) {
|
||||||
|
domainRequestTotals.set(domain, (domainRequestTotals.get(domain) || 0) + count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map route name → domains from route config
|
||||||
const routeDomains = new Map<string, string[]>();
|
const routeDomains = new Map<string, string[]>();
|
||||||
if (this.dcRouter.smartProxy) {
|
if (this.dcRouter.smartProxy) {
|
||||||
for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
|
for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
|
||||||
@@ -738,34 +747,26 @@ export class MetricsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use protocol cache to discover actual active domains (resolves wildcards)
|
// Resolve wildcards using domains seen in request metrics
|
||||||
const activeDomains = new Set<string>();
|
const allKnownDomains = new Set<string>(domainRequestTotals.keys());
|
||||||
const domainToBackend = new Map<string, string>(); // domain → host:port
|
|
||||||
for (const entry of protocolCache) {
|
for (const entry of protocolCache) {
|
||||||
if (entry.domain) {
|
if (entry.domain) allKnownDomains.add(entry.domain);
|
||||||
activeDomains.add(entry.domain);
|
|
||||||
domainToBackend.set(entry.domain, `${entry.host}:${entry.port}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build reverse map: domain → route name(s) that handle it
|
// Build reverse map: concrete domain → route name(s)
|
||||||
// For concrete domains: direct lookup from route config
|
|
||||||
// For wildcard patterns: match active domains from protocol cache
|
|
||||||
const domainToRoutes = new Map<string, string[]>();
|
const domainToRoutes = new Map<string, string[]>();
|
||||||
for (const [routeName, domains] of routeDomains) {
|
for (const [routeName, domains] of routeDomains) {
|
||||||
for (const pattern of domains) {
|
for (const pattern of domains) {
|
||||||
if (pattern.includes('*')) {
|
if (pattern.includes('*')) {
|
||||||
// Wildcard pattern — match against active domains from protocol cache
|
|
||||||
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$');
|
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$');
|
||||||
for (const activeDomain of activeDomains) {
|
for (const knownDomain of allKnownDomains) {
|
||||||
if (regex.test(activeDomain)) {
|
if (regex.test(knownDomain)) {
|
||||||
const existing = domainToRoutes.get(activeDomain);
|
const existing = domainToRoutes.get(knownDomain);
|
||||||
if (existing) { existing.push(routeName); }
|
if (existing) { existing.push(routeName); }
|
||||||
else { domainToRoutes.set(activeDomain, [routeName]); }
|
else { domainToRoutes.set(knownDomain, [routeName]); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Concrete domain
|
|
||||||
const existing = domainToRoutes.get(pattern);
|
const existing = domainToRoutes.get(pattern);
|
||||||
if (existing) { existing.push(routeName); }
|
if (existing) { existing.push(routeName); }
|
||||||
else { domainToRoutes.set(pattern, [routeName]); }
|
else { domainToRoutes.set(pattern, [routeName]); }
|
||||||
@@ -773,37 +774,40 @@ export class MetricsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate metrics per domain
|
// For each route, compute the total request count across all its resolved domains
|
||||||
// For each domain, sum metrics from all routes that serve it,
|
// so we can distribute throughput/connections proportionally
|
||||||
// divided by the number of domains each route serves
|
const routeTotalRequests = new Map<string, number>();
|
||||||
|
for (const [domain, routeNames] of domainToRoutes) {
|
||||||
|
const reqs = domainRequestTotals.get(domain) || 0;
|
||||||
|
for (const routeName of routeNames) {
|
||||||
|
routeTotalRequests.set(routeName, (routeTotalRequests.get(routeName) || 0) + reqs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate metrics per domain using request-count-proportional splitting
|
||||||
const domainAgg = new Map<string, {
|
const domainAgg = new Map<string, {
|
||||||
activeConnections: number;
|
activeConnections: number;
|
||||||
bytesInPerSec: number;
|
bytesInPerSec: number;
|
||||||
bytesOutPerSec: number;
|
bytesOutPerSec: number;
|
||||||
routeCount: number;
|
routeCount: number;
|
||||||
|
requestCount: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Track which routes are accounted for
|
|
||||||
const accountedRoutes = new Set<string>();
|
|
||||||
|
|
||||||
for (const [domain, routeNames] of domainToRoutes) {
|
for (const [domain, routeNames] of domainToRoutes) {
|
||||||
|
const domainReqs = domainRequestTotals.get(domain) || 0;
|
||||||
let totalConns = 0;
|
let totalConns = 0;
|
||||||
let totalIn = 0;
|
let totalIn = 0;
|
||||||
let totalOut = 0;
|
let totalOut = 0;
|
||||||
|
|
||||||
for (const routeName of routeNames) {
|
for (const routeName of routeNames) {
|
||||||
accountedRoutes.add(routeName);
|
|
||||||
const conns = connectionsByRoute.get(routeName) || 0;
|
const conns = connectionsByRoute.get(routeName) || 0;
|
||||||
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
|
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
|
||||||
// Count how many resolved domains share this route
|
const routeTotal = routeTotalRequests.get(routeName) || 0;
|
||||||
let domainsInRoute = 0;
|
|
||||||
for (const [, routes] of domainToRoutes) {
|
const share = routeTotal > 0 ? domainReqs / routeTotal : 0;
|
||||||
if (routes.includes(routeName)) domainsInRoute++;
|
totalConns += conns * share;
|
||||||
}
|
totalIn += tp.in * share;
|
||||||
const share = Math.max(domainsInRoute, 1);
|
totalOut += tp.out * share;
|
||||||
totalConns += conns / share;
|
|
||||||
totalIn += tp.in / share;
|
|
||||||
totalOut += tp.out / share;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
domainAgg.set(domain, {
|
domainAgg.set(domain, {
|
||||||
@@ -811,31 +815,10 @@ export class MetricsManager {
|
|||||||
bytesInPerSec: totalIn,
|
bytesInPerSec: totalIn,
|
||||||
bytesOutPerSec: totalOut,
|
bytesOutPerSec: totalOut,
|
||||||
routeCount: routeNames.length,
|
routeCount: routeNames.length,
|
||||||
|
requestCount: domainReqs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include routes with no domain config (fallback: use route name)
|
|
||||||
for (const [routeName, activeConns] of connectionsByRoute) {
|
|
||||||
if (accountedRoutes.has(routeName)) continue;
|
|
||||||
if (routeDomains.has(routeName)) continue; // has domains but no traffic matched
|
|
||||||
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
|
|
||||||
if (activeConns === 0 && tp.in === 0 && tp.out === 0) continue;
|
|
||||||
const existing = domainAgg.get(routeName);
|
|
||||||
if (existing) {
|
|
||||||
existing.activeConnections += activeConns;
|
|
||||||
existing.bytesInPerSec += tp.in;
|
|
||||||
existing.bytesOutPerSec += tp.out;
|
|
||||||
existing.routeCount++;
|
|
||||||
} else {
|
|
||||||
domainAgg.set(routeName, {
|
|
||||||
activeConnections: activeConns,
|
|
||||||
bytesInPerSec: tp.in,
|
|
||||||
bytesOutPerSec: tp.out,
|
|
||||||
routeCount: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const domainActivity = Array.from(domainAgg.entries())
|
const domainActivity = Array.from(domainAgg.entries())
|
||||||
.map(([domain, data]) => ({
|
.map(([domain, data]) => ({
|
||||||
domain,
|
domain,
|
||||||
@@ -843,6 +826,7 @@ export class MetricsManager {
|
|||||||
bytesOutPerSecond: data.bytesOutPerSec,
|
bytesOutPerSecond: data.bytesOutPerSec,
|
||||||
activeConnections: data.activeConnections,
|
activeConnections: data.activeConnections,
|
||||||
routeCount: data.routeCount,
|
routeCount: data.routeCount,
|
||||||
|
requestCount: data.requestCount,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
|
.sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export interface IDomainActivity {
|
|||||||
bytesOutPerSecond: number;
|
bytesOutPerSecond: number;
|
||||||
activeConnections: number;
|
activeConnections: number;
|
||||||
routeCount: number;
|
routeCount: number;
|
||||||
|
requestCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INetworkMetrics {
|
export interface INetworkMetrics {
|
||||||
|
|||||||
+10
-10
@@ -95,30 +95,30 @@ export async function createMigrationRunner(
|
|||||||
})
|
})
|
||||||
.step('unify-routes-rename-collection')
|
.step('unify-routes-rename-collection')
|
||||||
.from('13.8.2').to('13.16.0')
|
.from('13.8.2').to('13.16.0')
|
||||||
.description('Rename storedroutedoc → routedoc, add origin field, drop routeoverridedoc')
|
.description('Rename StoredRouteDoc → RouteDoc, add origin field, drop RouteOverrideDoc')
|
||||||
.up(async (ctx) => {
|
.up(async (ctx) => {
|
||||||
const db = ctx.mongo!;
|
const db = ctx.mongo!;
|
||||||
|
|
||||||
// 1. Rename storedroutedoc → routedoc
|
// 1. Rename StoredRouteDoc → RouteDoc (smartdata uses exact class names)
|
||||||
const collections = await db.listCollections({ name: 'storedroutedoc' }).toArray();
|
const collections = await db.listCollections({ name: 'StoredRouteDoc' }).toArray();
|
||||||
if (collections.length > 0) {
|
if (collections.length > 0) {
|
||||||
await db.renameCollection('storedroutedoc', 'routedoc');
|
await db.renameCollection('StoredRouteDoc', 'RouteDoc');
|
||||||
ctx.log.log('info', 'Renamed storedroutedoc → routedoc');
|
ctx.log.log('info', 'Renamed StoredRouteDoc → RouteDoc');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Set origin='api' on all migrated docs (they were API-created)
|
// 2. Set origin='api' on all migrated docs (they were API-created)
|
||||||
const routeCol = db.collection('routedoc');
|
const routeCol = db.collection('RouteDoc');
|
||||||
const result = await routeCol.updateMany(
|
const result = await routeCol.updateMany(
|
||||||
{ origin: { $exists: false } },
|
{ origin: { $exists: false } },
|
||||||
{ $set: { origin: 'api' } },
|
{ $set: { origin: 'api' } },
|
||||||
);
|
);
|
||||||
ctx.log.log('info', `Set origin='api' on ${result.modifiedCount} migrated route(s)`);
|
ctx.log.log('info', `Set origin='api' on ${result.modifiedCount} migrated route(s)`);
|
||||||
|
|
||||||
// 3. Drop routeoverridedoc collection
|
// 3. Drop RouteOverrideDoc collection
|
||||||
const overrideCollections = await db.listCollections({ name: 'routeoverridedoc' }).toArray();
|
const overrideCollections = await db.listCollections({ name: 'RouteOverrideDoc' }).toArray();
|
||||||
if (overrideCollections.length > 0) {
|
if (overrideCollections.length > 0) {
|
||||||
await db.collection('routeoverridedoc').drop();
|
await db.collection('RouteOverrideDoc').drop();
|
||||||
ctx.log.log('info', 'Dropped routeoverridedoc collection');
|
ctx.log.log('info', 'Dropped RouteOverrideDoc collection');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.16.0',
|
version: '13.17.3',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -560,11 +560,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
|
'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
|
||||||
'Transferred / min': this.formatBytes(totalBytesPerMin),
|
'Transferred / min': this.formatBytes(totalBytesPerMin),
|
||||||
'Connections': item.activeConnections,
|
'Connections': item.activeConnections,
|
||||||
|
'Requests': item.requestCount?.toLocaleString() ?? '0',
|
||||||
'Routes': item.routeCount,
|
'Routes': item.routeCount,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
heading1="Domain Activity"
|
heading1="Domain Activity"
|
||||||
heading2="Per-domain network activity aggregated from route metrics"
|
heading2="Per-domain network activity from request-level metrics"
|
||||||
searchable
|
searchable
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.pagination=${false}
|
.pagination=${false}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ function setupTlsVisibility(formEl: any) {
|
|||||||
|
|
||||||
@customElement('ops-view-routes')
|
@customElement('ops-view-routes')
|
||||||
export class OpsViewRoutes extends DeesElement {
|
export class OpsViewRoutes extends DeesElement {
|
||||||
|
@state() accessor routeFilter: 'User Routes' | 'System Routes' = 'User Routes';
|
||||||
|
|
||||||
@state() accessor routeState: appstate.IRouteManagementState = {
|
@state() accessor routeState: appstate.IRouteManagementState = {
|
||||||
mergedRoutes: [],
|
mergedRoutes: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
@@ -156,20 +158,20 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'configRoutes',
|
id: 'configRoutes',
|
||||||
title: 'From Config',
|
title: 'System Routes',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
value: configCount,
|
value: configCount,
|
||||||
icon: 'lucide:settings',
|
icon: 'lucide:settings',
|
||||||
description: 'Seeded from config/email/DNS',
|
description: 'From config, email, and DNS',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'apiRoutes',
|
id: 'apiRoutes',
|
||||||
title: 'API Created',
|
title: 'User Routes',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
value: apiCount,
|
value: apiCount,
|
||||||
icon: 'lucide:code',
|
icon: 'lucide:code',
|
||||||
description: 'Routes added via API',
|
description: 'Created via API',
|
||||||
color: '#0ea5e9',
|
color: '#0ea5e9',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -183,8 +185,14 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Map merged routes to sz-route-list-view format
|
// Filter routes based on selected tab
|
||||||
const szRoutes = mergedRoutes.map((mr) => {
|
const isUserRoutes = this.routeFilter === 'User Routes';
|
||||||
|
const filteredRoutes = mergedRoutes.filter((mr) =>
|
||||||
|
isUserRoutes ? mr.origin === 'api' : mr.origin !== 'api'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Map filtered routes to sz-route-list-view format
|
||||||
|
const szRoutes = filteredRoutes.map((mr) => {
|
||||||
const tags = [...(mr.route.tags || [])];
|
const tags = [...(mr.route.tags || [])];
|
||||||
tags.push(mr.origin);
|
tags.push(mr.origin);
|
||||||
if (!mr.enabled) tags.push('disabled');
|
if (!mr.enabled) tags.push('disabled');
|
||||||
@@ -218,6 +226,13 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
]}
|
]}
|
||||||
></dees-statsgrid>
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
<dees-input-multitoggle
|
||||||
|
class="routeFilterToggle"
|
||||||
|
.type=${'single'}
|
||||||
|
.options=${['User Routes', 'System Routes']}
|
||||||
|
.selectedOption=${this.routeFilter}
|
||||||
|
></dees-input-multitoggle>
|
||||||
|
|
||||||
${warnings.length > 0
|
${warnings.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="warnings-bar">
|
<div class="warnings-bar">
|
||||||
@@ -237,6 +252,7 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
? html`
|
? html`
|
||||||
<sz-route-list-view
|
<sz-route-list-view
|
||||||
.routes=${szRoutes}
|
.routes=${szRoutes}
|
||||||
|
.showActionsFilter=${isUserRoutes ? () => true : () => false}
|
||||||
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
||||||
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
|
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
|
||||||
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
|
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
|
||||||
@@ -244,8 +260,8 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>No routes configured</p>
|
<p>No ${isUserRoutes ? 'user' : 'system'} routes</p>
|
||||||
<p>Add a route to get started.</p>
|
<p>${isUserRoutes ? 'Add a route to get started.' : 'System routes are generated from config, email, and DNS settings.'}</p>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
@@ -661,5 +677,13 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
|
|
||||||
async firstUpdated() {
|
async firstUpdated() {
|
||||||
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
|
||||||
|
const toggle = this.shadowRoot!.querySelector('.routeFilterToggle') as any;
|
||||||
|
if (toggle) {
|
||||||
|
const sub = toggle.changeSubject.subscribe(() => {
|
||||||
|
this.routeFilter = toggle.selectedOption;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user