Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec0348a83c | |||
| 6c4adf70c7 | |||
| 29d6076355 | |||
| fa96a41e68 |
14
Dockerfile
14
Dockerfile
@@ -18,9 +18,17 @@ WORKDIR /app
|
|||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
ENV DCROUTER_MODE=OCI_CONTAINER
|
ENV DCROUTER_MODE=OCI_CONTAINER
|
||||||
|
ENV DCROUTER_HEAP_SIZE=512
|
||||||
|
ENV UV_THREADPOOL_SIZE=16
|
||||||
|
|
||||||
RUN pnpm install -g @servezone/healthy
|
RUN pnpm install -g @servezone/healthy
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ]
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 CMD [ "healthy" ]
|
||||||
|
|
||||||
EXPOSE 80
|
LABEL org.opencontainers.image.title="dcrouter" \
|
||||||
CMD ["npm", "start"]
|
org.opencontainers.image.description="Multi-service datacenter gateway" \
|
||||||
|
org.opencontainers.image.source="https://code.foss.global/serve.zone/dcrouter"
|
||||||
|
|
||||||
|
# HTTP/HTTPS, SMTP/Submission/SMTPS, DNS, RADIUS, OpsServer, RemoteIngress, dynamic range
|
||||||
|
EXPOSE 80 443 25 587 465 53/tcp 53/udp 1812/udp 1813/udp 3000 8443 29000-30000
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "node --max_old_space_size=${DCROUTER_HEAP_SIZE} ./cli.js"]
|
||||||
|
|||||||
17
changelog.md
17
changelog.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-27 - 11.12.0 - feat(web-ui)
|
||||||
|
pause dashboard polling, sockets, and chart updates when the tab is hidden
|
||||||
|
|
||||||
|
- replace interval-based auto-refresh with scheduled actions using visibility-aware auto-pause
|
||||||
|
- disconnect and reconnect the TypedSocket on tab visibility changes to avoid background log buildup
|
||||||
|
- batch pushed log entries per animation frame and add an in-flight refresh guard to reduce unnecessary re-renders and overlapping requests
|
||||||
|
- update state subscriptions to use select() and document the new tab visibility optimization behavior
|
||||||
|
- bump smartdb, smartproxy, smartstate, remoteingress, dees-element, and tstest dependencies
|
||||||
|
|
||||||
|
## 2026-03-26 - 11.11.0 - feat(docker,cache,proxy)
|
||||||
|
improve container runtime defaults and add configurable connection limits
|
||||||
|
|
||||||
|
- replace the embedded cache backend integration from smartmongo LocalTsmDb to smartdb LocalSmartDb
|
||||||
|
- add OCI container settings for heap size, threadpool size, expanded exposed ports, image metadata, and a direct node startup command
|
||||||
|
- introduce startup checks for file descriptor limits and warn when container nofile limits are too low for production
|
||||||
|
- set gateway-oriented SmartProxy default limits and allow max connections, per-IP connections, and rate limits to be configured through OCI environment variables
|
||||||
|
|
||||||
## 2026-03-26 - 11.10.7 - fix(sms)
|
## 2026-03-26 - 11.10.7 - fix(sms)
|
||||||
update sms service to use async ProjectInfo initialization
|
update sms service to use async ProjectInfo initialization
|
||||||
|
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.10.7",
|
"version": "11.12.0",
|
||||||
"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": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --logfile --timeout 60)",
|
"test": "(tstest test/ --logfile --timeout 60)",
|
||||||
"start": "(node --max_old_space_size=250 ./cli.js)",
|
"start": "(node ./cli.js)",
|
||||||
"startTs": "(node cli.ts.js)",
|
"startTs": "(node cli.ts.js)",
|
||||||
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
||||||
"build:docker": "tsdocker build --verbose",
|
"build:docker": "tsdocker build --verbose",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"@git.zone/tsbuild": "^4.4.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.10.0",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.2",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^3.6.1",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@git.zone/tswatch": "^3.3.2",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@types/node": "^25.5.0"
|
"@types/node": "^25.5.0"
|
||||||
},
|
},
|
||||||
@@ -36,33 +36,33 @@
|
|||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.49.0",
|
"@design.estate/dees-catalog": "^3.49.0",
|
||||||
"@design.estate/dees-element": "^2.2.3",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@push.rocks/lik": "^6.4.0",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@push.rocks/projectinfo": "^5.1.0",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^9.3.0",
|
"@push.rocks/smartacme": "^9.3.0",
|
||||||
"@push.rocks/smartdata": "^7.1.3",
|
"@push.rocks/smartdata": "^7.1.3",
|
||||||
|
"@push.rocks/smartdb": "^2.0.0",
|
||||||
"@push.rocks/smartdns": "^7.9.0",
|
"@push.rocks/smartdns": "^7.9.0",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartmetrics": "^3.0.3",
|
"@push.rocks/smartmetrics": "^3.0.3",
|
||||||
"@push.rocks/smartmongo": "^5.1.1",
|
|
||||||
"@push.rocks/smartmta": "^5.3.1",
|
"@push.rocks/smartmta": "^5.3.1",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@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": "^26.2.4",
|
"@push.rocks/smartproxy": "^27.0.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",
|
||||||
"@push.rocks/smartstate": "^2.2.1",
|
"@push.rocks/smartstate": "^2.3.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.9.0",
|
"@serve.zone/catalog": "^2.9.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.14.3",
|
"@serve.zone/remoteingress": "^4.15.3",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"lru-cache": "^11.2.7",
|
"lru-cache": "^11.2.7",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
|
|||||||
513
pnpm-lock.yaml
generated
513
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
62
readme.md
62
readme.md
@@ -83,7 +83,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
|
|
||||||
### 💾 Persistent Storage & Caching
|
### 💾 Persistent Storage & Caching
|
||||||
- **Multiple storage backends**: filesystem, custom functions, or in-memory
|
- **Multiple storage backends**: filesystem, custom functions, or in-memory
|
||||||
- **Embedded cache database** via smartdata + LocalTsmDb (MongoDB-compatible)
|
- **Embedded cache database** via smartdata + smartdb (MongoDB-compatible)
|
||||||
- **Automatic TTL-based cleanup** for cached emails and IP reputation data
|
- **Automatic TTL-based cleanup** for cached emails and IP reputation data
|
||||||
|
|
||||||
### 🖥️ OpsServer Dashboard
|
### 🖥️ OpsServer Dashboard
|
||||||
@@ -93,6 +93,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
||||||
- **Remote ingress management** with connection token generation and one-click copy
|
- **Remote ingress management** with connection token generation and one-click copy
|
||||||
- **Read-only configuration display** — DcRouter is configured through code
|
- **Read-only configuration display** — DcRouter is configured through code
|
||||||
|
- **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
|
### 🔧 Programmatic API Client
|
||||||
- **Object-oriented API** — resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) with instance methods
|
- **Object-oriented API** — resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) with instance methods
|
||||||
@@ -340,7 +341,7 @@ graph TB
|
|||||||
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and 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) |
|
| **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) |
|
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
|
||||||
| **CacheDb** | `@push.rocks/smartdata` | Embedded MongoDB-compatible database (LocalTsmDb) for persistent caching |
|
| **CacheDb** | `@push.rocks/smartdb` | Embedded MongoDB-compatible database (LocalSmartDb) for persistent caching |
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
@@ -1066,7 +1067,7 @@ Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, I
|
|||||||
|
|
||||||
### Cache Database
|
### Cache Database
|
||||||
|
|
||||||
An embedded MongoDB-compatible database (via smartdata + LocalTsmDb) for persistent caching with automatic TTL cleanup:
|
An embedded MongoDB-compatible database (via smartdata + smartdb) for persistent caching with automatic TTL cleanup:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
cacheConfig: {
|
cacheConfig: {
|
||||||
@@ -1406,37 +1407,58 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
|||||||
|
|
||||||
## Docker / OCI Container Deployment
|
## Docker / OCI Container Deployment
|
||||||
|
|
||||||
DcRouter ships with a `Dockerfile` and supports environment-variable-driven configuration for OCI container deployments. When `DCROUTER_MODE=OCI_CONTAINER` is set, DcRouter automatically reads configuration from environment variables (and optionally from a JSON config file).
|
DcRouter ships with a production-ready `Dockerfile` and supports environment-variable-driven configuration for OCI container deployments. The container image includes tini as PID 1 (via the base image), proper health checks, and configurable resource limits. When `DCROUTER_MODE=OCI_CONTAINER` is set, DcRouter automatically reads configuration from environment variables (and optionally from a JSON config file).
|
||||||
|
|
||||||
### Running with Docker
|
### Running with Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-e DCROUTER_MODE=OCI_CONTAINER \
|
--ulimit nofile=65536:65536 \
|
||||||
-e DCROUTER_TLS_EMAIL=admin@example.com \
|
-e DCROUTER_TLS_EMAIL=admin@example.com \
|
||||||
-e DCROUTER_PUBLIC_IP=203.0.113.1 \
|
-e DCROUTER_PUBLIC_IP=203.0.113.1 \
|
||||||
-e DCROUTER_DNS_NS_DOMAINS=ns1.example.com,ns2.example.com \
|
-e DCROUTER_DNS_NS_DOMAINS=ns1.example.com,ns2.example.com \
|
||||||
-e DCROUTER_DNS_SCOPES=example.com \
|
-e DCROUTER_DNS_SCOPES=example.com \
|
||||||
-p 80:80 -p 443:443 -p 25:25 -p 53:53/udp -p 3000:3000 \
|
-p 80:80 -p 443:443 -p 25:25 -p 587:587 -p 465:465 \
|
||||||
|
-p 53:53/udp -p 3000:3000 -p 8443:8443 \
|
||||||
code.foss.global/serve.zone/dcrouter:latest
|
code.foss.global/serve.zone/dcrouter:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> ⚡ **Production tip:** Always set `--ulimit nofile=65536:65536` for production deployments. DcRouter will log a warning at startup if the file descriptor limit is below 65536.
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Example |
|
| Variable | Description | Default | Example |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|---------|
|
||||||
| `DCROUTER_MODE` | Set to `OCI_CONTAINER` to enable container mode | `OCI_CONTAINER` |
|
| `DCROUTER_MODE` | Container mode (set automatically in image) | `OCI_CONTAINER` | — |
|
||||||
| `DCROUTER_CONFIG_PATH` | Path to a JSON config file (loaded as base, env vars override) | `/config/dcrouter.json` |
|
| `DCROUTER_CONFIG_PATH` | Path to JSON config file (env vars override) | — | `/config/dcrouter.json` |
|
||||||
| `DCROUTER_BASE_DIR` | Override base data directory | `/data/dcrouter` |
|
| `DCROUTER_BASE_DIR` | Base data directory | `~/.serve.zone/dcrouter` | `/data/dcrouter` |
|
||||||
| `DCROUTER_TLS_EMAIL` | ACME contact email | `admin@example.com` |
|
| `DCROUTER_TLS_EMAIL` | ACME contact email | — | `admin@example.com` |
|
||||||
| `DCROUTER_TLS_DOMAIN` | Primary TLS domain | `example.com` |
|
| `DCROUTER_TLS_DOMAIN` | Primary TLS domain | — | `example.com` |
|
||||||
| `DCROUTER_PUBLIC_IP` | Public IP for DNS records | `203.0.113.1` |
|
| `DCROUTER_PUBLIC_IP` | Public IP for DNS records | — | `203.0.113.1` |
|
||||||
| `DCROUTER_PROXY_IPS` | Comma-separated ingress proxy IPs | `198.51.100.1,198.51.100.2` |
|
| `DCROUTER_PROXY_IPS` | Comma-separated ingress proxy IPs | — | `198.51.100.1,198.51.100.2` |
|
||||||
| `DCROUTER_DNS_NS_DOMAINS` | Comma-separated nameserver domains | `ns1.example.com,ns2.example.com` |
|
| `DCROUTER_DNS_NS_DOMAINS` | Comma-separated nameserver domains | — | `ns1.example.com,ns2.example.com` |
|
||||||
| `DCROUTER_DNS_SCOPES` | Comma-separated authoritative domains | `example.com,other.com` |
|
| `DCROUTER_DNS_SCOPES` | Comma-separated authoritative domains | — | `example.com,other.com` |
|
||||||
| `DCROUTER_EMAIL_HOSTNAME` | SMTP server hostname | `mail.example.com` |
|
| `DCROUTER_EMAIL_HOSTNAME` | SMTP server hostname | — | `mail.example.com` |
|
||||||
| `DCROUTER_EMAIL_PORTS` | Comma-separated email ports | `25,587,465` |
|
| `DCROUTER_EMAIL_PORTS` | Comma-separated email ports | — | `25,587,465` |
|
||||||
| `DCROUTER_CACHE_ENABLED` | Enable/disable cache database | `true` |
|
| `DCROUTER_CACHE_ENABLED` | Enable/disable cache database | `true` | `false` |
|
||||||
|
| `DCROUTER_HEAP_SIZE` | Node.js V8 heap size in MB | `512` | `1024` |
|
||||||
|
| `DCROUTER_MAX_CONNECTIONS` | Global max concurrent connections | `50000` | `100000` |
|
||||||
|
| `DCROUTER_MAX_CONNECTIONS_PER_IP` | Max connections per source IP | `100` | `200` |
|
||||||
|
| `DCROUTER_CONNECTION_RATE_LIMIT` | Max new connections/min per IP | `600` | `1200` |
|
||||||
|
|
||||||
|
### Exposed Ports
|
||||||
|
|
||||||
|
The container exposes all service ports:
|
||||||
|
|
||||||
|
| Port(s) | Protocol | Service |
|
||||||
|
|---------|----------|---------|
|
||||||
|
| 80, 443 | TCP | HTTP/HTTPS (SmartProxy) |
|
||||||
|
| 25, 587, 465 | TCP | SMTP, Submission, SMTPS |
|
||||||
|
| 53 | TCP/UDP | DNS |
|
||||||
|
| 1812, 1813 | UDP | RADIUS auth/acct |
|
||||||
|
| 3000 | TCP | OpsServer dashboard |
|
||||||
|
| 8443 | TCP | Remote ingress tunnels |
|
||||||
|
| 29000–30000 | TCP | Dynamic port range |
|
||||||
|
|
||||||
### Building the Image
|
### Building the Image
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.10.7',
|
version: '11.12.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
26
ts/cache/classes.cachedb.ts
vendored
26
ts/cache/classes.cachedb.ts
vendored
@@ -15,15 +15,15 @@ export interface ICacheDbOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CacheDb - Wrapper around LocalTsmDb and smartdata
|
* CacheDb - Wrapper around LocalSmartDb and smartdata
|
||||||
*
|
*
|
||||||
* Provides persistent caching using smartdata as the ORM layer
|
* Provides persistent caching using smartdata as the ORM layer
|
||||||
* and LocalTsmDb as the embedded database engine.
|
* and LocalSmartDb as the embedded database engine.
|
||||||
*/
|
*/
|
||||||
export class CacheDb {
|
export class CacheDb {
|
||||||
private static instance: CacheDb | null = null;
|
private static instance: CacheDb | null = null;
|
||||||
|
|
||||||
private localTsmDb!: plugins.smartmongo.LocalTsmDb;
|
private localSmartDb!: plugins.smartdb.LocalSmartDb;
|
||||||
private smartdataDb!: plugins.smartdata.SmartdataDb;
|
private smartdataDb!: plugins.smartdata.SmartdataDb;
|
||||||
private options: Required<ICacheDbOptions>;
|
private options: Required<ICacheDbOptions>;
|
||||||
private isStarted: boolean = false;
|
private isStarted: boolean = false;
|
||||||
@@ -55,8 +55,8 @@ export class CacheDb {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the cache database
|
* Start the cache database
|
||||||
* - Initializes LocalTsmDb with file persistence
|
* - Initializes LocalSmartDb with file persistence
|
||||||
* - Connects smartdata to the LocalTsmDb via Unix socket
|
* - Connects smartdata to the LocalSmartDb via Unix socket
|
||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
if (this.isStarted) {
|
if (this.isStarted) {
|
||||||
@@ -68,16 +68,16 @@ export class CacheDb {
|
|||||||
// Ensure storage directory exists
|
// Ensure storage directory exists
|
||||||
await plugins.fsUtils.ensureDir(this.options.storagePath);
|
await plugins.fsUtils.ensureDir(this.options.storagePath);
|
||||||
|
|
||||||
// Create LocalTsmDb instance
|
// Create LocalSmartDb instance
|
||||||
this.localTsmDb = new plugins.smartmongo.LocalTsmDb({
|
this.localSmartDb = new plugins.smartdb.LocalSmartDb({
|
||||||
folderPath: this.options.storagePath,
|
folderPath: this.options.storagePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start LocalTsmDb and get connection info
|
// Start LocalSmartDb and get connection info
|
||||||
const connectionInfo = await this.localTsmDb.start();
|
const connectionInfo = await this.localSmartDb.start();
|
||||||
|
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
logger.log('debug', `LocalTsmDb started with URI: ${connectionInfo.connectionUri}`);
|
logger.log('debug', `LocalSmartDb started with URI: ${connectionInfo.connectionUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize smartdata with the connection URI
|
// Initialize smartdata with the connection URI
|
||||||
@@ -109,9 +109,9 @@ export class CacheDb {
|
|||||||
await this.smartdataDb.close();
|
await this.smartdataDb.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop LocalTsmDb
|
// Stop LocalSmartDb
|
||||||
if (this.localTsmDb) {
|
if (this.localSmartDb) {
|
||||||
await this.localTsmDb.stop();
|
await this.localSmartDb.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isStarted = false;
|
this.isStarted = false;
|
||||||
|
|||||||
@@ -528,10 +528,36 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
|
await this.checkSystemLimits();
|
||||||
logger.log('info', 'Starting DcRouter Services');
|
logger.log('info', 'Starting DcRouter Services');
|
||||||
await this.serviceManager.start();
|
await this.serviceManager.start();
|
||||||
this.logStartupSummary();
|
this.logStartupSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect OS-level resource limits and warn if they are too low for production use.
|
||||||
|
* This is detection only — no attempts to raise limits.
|
||||||
|
*/
|
||||||
|
private async checkSystemLimits(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||||
|
const limitsContent = await fs.file('/proc/self/limits').encoding('utf8').read() as string;
|
||||||
|
const nofileLine = limitsContent.split('\n').find((line: string) => line.startsWith('Max open files'));
|
||||||
|
if (nofileLine) {
|
||||||
|
const parts = nofileLine.split(/\s{2,}/);
|
||||||
|
const softLimit = parseInt(parts[1], 10);
|
||||||
|
const hardLimit = parseInt(parts[2], 10);
|
||||||
|
if (softLimit < 65536) {
|
||||||
|
logger.log('warn', `File descriptor soft limit is ${softLimit} (hard: ${hardLimit}). ` +
|
||||||
|
`For production use, set --ulimit nofile=65536:65536 on the container runtime.`);
|
||||||
|
} else {
|
||||||
|
logger.log('info', `File descriptor limits: soft=${softLimit}, hard=${hardLimit}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-Linux or /proc not available — silently skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log comprehensive startup summary
|
* Log comprehensive startup summary
|
||||||
@@ -708,9 +734,28 @@ export class DcRouter {
|
|||||||
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
||||||
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
||||||
|
|
||||||
// Create SmartProxy configuration
|
// Create SmartProxy configuration with sensible gateway defaults.
|
||||||
|
// User's smartProxyConfig overrides these defaults via spread.
|
||||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||||
|
// --- dcrouter gateway defaults ---
|
||||||
|
maxConnectionsPerIP: 100,
|
||||||
|
connectionRateLimitPerMinute: 600,
|
||||||
|
socketTimeout: 120_000,
|
||||||
|
inactivityTimeout: 120_000,
|
||||||
|
keepAlive: true,
|
||||||
|
noDelay: true,
|
||||||
|
gracefulShutdownTimeout: 30_000,
|
||||||
|
// --- user overrides ---
|
||||||
...this.options.smartProxyConfig,
|
...this.options.smartProxyConfig,
|
||||||
|
// --- deep-merge defaults.security so user can override maxConnections ---
|
||||||
|
defaults: {
|
||||||
|
...this.options.smartProxyConfig?.defaults,
|
||||||
|
security: {
|
||||||
|
maxConnections: 50_000,
|
||||||
|
...this.options.smartProxyConfig?.defaults?.security,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// --- always set by dcrouter (after spread) ---
|
||||||
routes,
|
routes,
|
||||||
acme: acmeConfig,
|
acme: acmeConfig,
|
||||||
certStore: {
|
certStore: {
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ import * as qenv from '@push.rocks/qenv';
|
|||||||
import * as smartacme from '@push.rocks/smartacme';
|
import * as smartacme from '@push.rocks/smartacme';
|
||||||
import * as smartdata from '@push.rocks/smartdata';
|
import * as smartdata from '@push.rocks/smartdata';
|
||||||
import * as smartdns from '@push.rocks/smartdns';
|
import * as smartdns from '@push.rocks/smartdns';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfs from '@push.rocks/smartfs';
|
||||||
import * as smartguard from '@push.rocks/smartguard';
|
import * as smartguard from '@push.rocks/smartguard';
|
||||||
import * as smartjwt from '@push.rocks/smartjwt';
|
import * as smartjwt from '@push.rocks/smartjwt';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartmetrics from '@push.rocks/smartmetrics';
|
import * as smartmetrics from '@push.rocks/smartmetrics';
|
||||||
import * as smartmta from '@push.rocks/smartmta';
|
import * as smartmta from '@push.rocks/smartmta';
|
||||||
import * as smartmongo from '@push.rocks/smartmongo';
|
import * as smartdb from '@push.rocks/smartdb';
|
||||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
import * as smartproxy from '@push.rocks/smartproxy';
|
import * as smartproxy from '@push.rocks/smartproxy';
|
||||||
@@ -64,7 +64,7 @@ import * as smartrx from '@push.rocks/smartrx';
|
|||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmetrics, smartmongo, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, taskbuffer };
|
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, taskbuffer };
|
||||||
|
|
||||||
// Define SmartLog types for use in error handling
|
// Define SmartLog types for use in error handling
|
||||||
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
||||||
@@ -90,7 +90,7 @@ export {
|
|||||||
uuid,
|
uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filesystem utilities (compatibility helpers for smartfile v13+)
|
// Filesystem utilities
|
||||||
export const fsUtils = {
|
export const fsUtils = {
|
||||||
/**
|
/**
|
||||||
* Ensure a directory exists, creating it recursively if needed (sync)
|
* Ensure a directory exists, creating it recursively if needed (sync)
|
||||||
|
|||||||
@@ -96,5 +96,27 @@ export function getOciContainerConfig(): IDcRouterOptions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection capacity config
|
||||||
|
const maxConnections = process.env.DCROUTER_MAX_CONNECTIONS;
|
||||||
|
const maxConnectionsPerIP = process.env.DCROUTER_MAX_CONNECTIONS_PER_IP;
|
||||||
|
const connectionRateLimit = process.env.DCROUTER_CONNECTION_RATE_LIMIT;
|
||||||
|
if (maxConnections || maxConnectionsPerIP || connectionRateLimit) {
|
||||||
|
options.smartProxyConfig = {
|
||||||
|
...options.smartProxyConfig,
|
||||||
|
routes: options.smartProxyConfig?.routes || [],
|
||||||
|
...(maxConnectionsPerIP ? { maxConnectionsPerIP: parseInt(maxConnectionsPerIP, 10) } : {}),
|
||||||
|
...(connectionRateLimit ? { connectionRateLimitPerMinute: parseInt(connectionRateLimit, 10) } : {}),
|
||||||
|
...(maxConnections ? {
|
||||||
|
defaults: {
|
||||||
|
...options.smartProxyConfig?.defaults,
|
||||||
|
security: {
|
||||||
|
...options.smartProxyConfig?.defaults?.security,
|
||||||
|
maxConnections: parseInt(maxConnections, 10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.10.7',
|
version: '11.12.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1186,18 +1186,33 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
|||||||
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
||||||
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
// Batched log entry handler — buffers incoming entries and flushes once per animation frame
|
||||||
|
let logEntryBuffer: interfaces.data.ILogEntry[] = [];
|
||||||
|
let logFlushScheduled = false;
|
||||||
|
|
||||||
|
function flushLogEntries() {
|
||||||
|
logFlushScheduled = false;
|
||||||
|
if (logEntryBuffer.length === 0) return;
|
||||||
|
const current = logStatePart.getState()!;
|
||||||
|
const updated = [...current.recentLogs, ...logEntryBuffer];
|
||||||
|
logEntryBuffer = [];
|
||||||
|
// Cap at 2000 entries
|
||||||
|
if (updated.length > 2000) {
|
||||||
|
updated.splice(0, updated.length - 2000);
|
||||||
|
}
|
||||||
|
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
|
||||||
|
}
|
||||||
|
|
||||||
// Register handler for pushed log entries from the server
|
// Register handler for pushed log entries from the server
|
||||||
socketRouter.addTypedHandler(
|
socketRouter.addTypedHandler(
|
||||||
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
|
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
|
||||||
'pushLogEntry',
|
'pushLogEntry',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const current = logStatePart.getState()!;
|
logEntryBuffer.push(dataArg.entry);
|
||||||
const updated = [...current.recentLogs, dataArg.entry];
|
if (!logFlushScheduled) {
|
||||||
// Cap at 2000 entries
|
logFlushScheduled = true;
|
||||||
if (updated.length > 2000) {
|
requestAnimationFrame(flushLogEntries);
|
||||||
updated.splice(0, updated.length - 2000);
|
|
||||||
}
|
}
|
||||||
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1228,8 +1243,21 @@ async function disconnectSocket() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In-flight guard to prevent concurrent refresh requests
|
||||||
|
let isRefreshing = false;
|
||||||
|
|
||||||
// Combined refresh action for efficient polling
|
// Combined refresh action for efficient polling
|
||||||
async function dispatchCombinedRefreshAction() {
|
async function dispatchCombinedRefreshAction() {
|
||||||
|
if (isRefreshing) return;
|
||||||
|
isRefreshing = true;
|
||||||
|
try {
|
||||||
|
await dispatchCombinedRefreshActionInner();
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatchCombinedRefreshActionInner() {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
if (!context.identity) return;
|
if (!context.identity) return;
|
||||||
const currentView = uiStatePart.getState()!.activeView;
|
const currentView = uiStatePart.getState()!.activeView;
|
||||||
@@ -1355,48 +1383,48 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize auto-refresh
|
// Create a proper action for the combined refresh so we can use createScheduledAction
|
||||||
let refreshInterval: NodeJS.Timeout | null = null;
|
const combinedRefreshAction = statsStatePart.createAction<void>(async (statePartArg) => {
|
||||||
let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts
|
await dispatchCombinedRefreshAction();
|
||||||
|
// Return current state — dispatchCombinedRefreshAction already updates all state parts directly
|
||||||
|
return statePartArg.getState()!;
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize auto-refresh when UI state is ready
|
// Scheduled refresh process with autoPause: 'visibility' — automatically pauses when tab is hidden
|
||||||
(() => {
|
let refreshProcess: ReturnType<typeof statsStatePart.createScheduledAction> | null = null;
|
||||||
const startAutoRefresh = () => {
|
|
||||||
const uiState = uiStatePart.getState()!;
|
|
||||||
const loginState = loginStatePart.getState()!;
|
|
||||||
|
|
||||||
// Only start if conditions are met and not already running at the same rate
|
const startAutoRefresh = () => {
|
||||||
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
const uiState = uiStatePart.getState()!;
|
||||||
// Check if we need to restart the interval (rate changed or not running)
|
const loginState = loginStatePart.getState()!;
|
||||||
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
|
|
||||||
stopAutoRefresh();
|
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||||
currentRefreshRate = uiState.refreshInterval;
|
// Dispose old process if interval changed or not running
|
||||||
refreshInterval = setInterval(() => {
|
if (refreshProcess) {
|
||||||
// Use combined refresh action for efficiency
|
refreshProcess.dispose();
|
||||||
dispatchCombinedRefreshAction();
|
refreshProcess = null;
|
||||||
}, uiState.refreshInterval);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stopAutoRefresh();
|
|
||||||
}
|
}
|
||||||
};
|
refreshProcess = statsStatePart.createScheduledAction({
|
||||||
|
action: combinedRefreshAction,
|
||||||
const stopAutoRefresh = () => {
|
payload: undefined,
|
||||||
if (refreshInterval) {
|
intervalMs: uiState.refreshInterval,
|
||||||
clearInterval(refreshInterval);
|
autoPause: 'visibility',
|
||||||
refreshInterval = null;
|
});
|
||||||
currentRefreshRate = 0;
|
} else {
|
||||||
|
if (refreshProcess) {
|
||||||
|
refreshProcess.dispose();
|
||||||
|
refreshProcess = null;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Watch for relevant changes only
|
// Watch for relevant changes
|
||||||
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
|
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
|
||||||
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
|
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
|
||||||
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
|
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
|
||||||
|
|
||||||
uiStatePart.state.subscribe((state) => {
|
uiStatePart.select((s) => ({ autoRefresh: s.autoRefresh, refreshInterval: s.refreshInterval }))
|
||||||
// Only restart if relevant values changed
|
.subscribe((state) => {
|
||||||
if (state.autoRefresh !== previousAutoRefresh ||
|
if (state.autoRefresh !== previousAutoRefresh ||
|
||||||
state.refreshInterval !== previousRefreshInterval) {
|
state.refreshInterval !== previousRefreshInterval) {
|
||||||
previousAutoRefresh = state.autoRefresh;
|
previousAutoRefresh = state.autoRefresh;
|
||||||
previousRefreshInterval = state.refreshInterval;
|
previousRefreshInterval = state.refreshInterval;
|
||||||
@@ -1404,26 +1432,33 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loginStatePart.state.subscribe((state) => {
|
loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
|
||||||
// Only restart if login state changed
|
if (isLoggedIn !== previousIsLoggedIn) {
|
||||||
if (state.isLoggedIn !== previousIsLoggedIn) {
|
previousIsLoggedIn = isLoggedIn;
|
||||||
previousIsLoggedIn = state.isLoggedIn;
|
startAutoRefresh();
|
||||||
startAutoRefresh();
|
|
||||||
|
|
||||||
// Connect/disconnect TypedSocket based on login state
|
// Connect/disconnect TypedSocket based on login state
|
||||||
if (state.isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
connectSocket();
|
connectSocket();
|
||||||
} else {
|
} else {
|
||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initial start
|
// Pause/resume WebSocket when tab visibility changes
|
||||||
startAutoRefresh();
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
// Connect TypedSocket if already logged in (e.g., persistent session)
|
disconnectSocket();
|
||||||
if (loginStatePart.getState()!.isLoggedIn) {
|
} else if (loginStatePart.getState()!.isLoggedIn) {
|
||||||
connectSocket();
|
connectSocket();
|
||||||
}
|
}
|
||||||
})();
|
});
|
||||||
|
|
||||||
|
// Initial start
|
||||||
|
startAutoRefresh();
|
||||||
|
|
||||||
|
// Connect TypedSocket if already logged in (e.g., persistent session)
|
||||||
|
if (loginStatePart.getState()!.isLoggedIn) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const sub = appstate.certificateStatePart.state.subscribe((newState) => {
|
const sub = appstate.certificateStatePart.select().subscribe((newState) => {
|
||||||
this.certState = newState;
|
this.certState = newState;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(sub);
|
this.rxSubscriptions.push(sub);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class OpsViewEmails extends DeesElement {
|
|||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
await super.connectedCallback();
|
await super.connectedCallback();
|
||||||
this.stateSubscription = appstate.emailOpsStatePart.state.subscribe((state) => {
|
this.stateSubscription = appstate.emailOpsStatePart.select().subscribe((state) => {
|
||||||
this.emails = state.emails;
|
this.emails = state.emails;
|
||||||
this.isLoading = state.isLoading;
|
this.isLoading = state.isLoading;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
// Track if we need to update the chart to avoid unnecessary re-renders
|
// Track if we need to update the chart to avoid unnecessary re-renders
|
||||||
private lastChartUpdate = 0;
|
private lastChartUpdate = 0;
|
||||||
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
||||||
|
|
||||||
private trafficUpdateTimer: any = null;
|
private trafficUpdateTimer: any = null;
|
||||||
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||||
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
||||||
|
private visibilityHandler: (() => void) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -59,28 +60,42 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
this.updateNetworkData();
|
this.updateNetworkData();
|
||||||
this.startTrafficUpdateTimer();
|
this.startTrafficUpdateTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
await super.connectedCallback();
|
await super.connectedCallback();
|
||||||
|
|
||||||
|
// Pause/resume traffic timer when tab visibility changes
|
||||||
|
this.visibilityHandler = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.stopTrafficUpdateTimer();
|
||||||
|
} else {
|
||||||
|
this.startTrafficUpdateTimer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', this.visibilityHandler);
|
||||||
|
|
||||||
// When network view becomes visible, ensure we fetch network data
|
// When network view becomes visible, ensure we fetch network data
|
||||||
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnectedCallback() {
|
async disconnectedCallback() {
|
||||||
await super.disconnectedCallback();
|
await super.disconnectedCallback();
|
||||||
this.stopTrafficUpdateTimer();
|
this.stopTrafficUpdateTimer();
|
||||||
|
if (this.visibilityHandler) {
|
||||||
|
document.removeEventListener('visibilitychange', this.visibilityHandler);
|
||||||
|
this.visibilityHandler = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToStateParts() {
|
private subscribeToStateParts() {
|
||||||
// Subscribe and track unsubscribe functions
|
// Subscribe and track unsubscribe functions
|
||||||
const statsUnsubscribe = appstate.statsStatePart.state.subscribe((state) => {
|
const statsUnsubscribe = appstate.statsStatePart.select().subscribe((state) => {
|
||||||
this.statsState = state;
|
this.statsState = state;
|
||||||
this.updateNetworkData();
|
this.updateNetworkData();
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(statsUnsubscribe);
|
this.rxSubscriptions.push(statsUnsubscribe);
|
||||||
|
|
||||||
const networkUnsubscribe = appstate.networkStatePart.state.subscribe((state) => {
|
const networkUnsubscribe = appstate.networkStatePart.select().subscribe((state) => {
|
||||||
this.networkState = state;
|
this.networkState = state;
|
||||||
this.updateNetworkData();
|
this.updateNetworkData();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const sub = appstate.remoteIngressStatePart.state.subscribe((newState) => {
|
const sub = appstate.remoteIngressStatePart.select().subscribe((newState) => {
|
||||||
this.riState = newState;
|
this.riState = newState;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(sub);
|
this.rxSubscriptions.push(sub);
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ ts_web/
|
|||||||
|
|
||||||
### State Management
|
### State Management
|
||||||
|
|
||||||
The app uses `@push.rocks/smartstate` with multiple state parts:
|
The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled actions with `autoPause: 'visibility'`, and batched updates:
|
||||||
|
|
||||||
| State Part | Mode | Description |
|
| State Part | Mode | Description |
|
||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
@@ -125,6 +125,16 @@ The app uses `@push.rocks/smartstate` with multiple state parts:
|
|||||||
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
|
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
|
||||||
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
|
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
|
||||||
|
|
||||||
|
### 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
|
### Actions
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupStateSync(): void {
|
private setupStateSync(): void {
|
||||||
appstate.uiStatePart.state.subscribe((uiState) => {
|
appstate.uiStatePart.select().subscribe((uiState) => {
|
||||||
if (this.suppressStateUpdate) return;
|
if (this.suppressStateUpdate) return;
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
|
|||||||
Reference in New Issue
Block a user