@serve.zone/dcrouter 11.10.7 → 11.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +514 -514
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cache/classes.cachedb.d.ts +5 -5
- package/dist_ts/cache/classes.cachedb.js +14 -14
- package/dist_ts/classes.dcrouter.d.ts +5 -0
- package/dist_ts/classes.dcrouter.js +48 -2
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +5 -5
- package/dist_ts_oci_container/index.js +22 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +97 -63
- package/dist_ts_web/elements/ops-view-certificates.js +2 -2
- package/dist_ts_web/elements/ops-view-emails.js +2 -2
- package/dist_ts_web/elements/ops-view-network.d.ts +1 -0
- package/dist_ts_web/elements/ops-view-network.js +18 -3
- package/dist_ts_web/elements/ops-view-remoteingress.js +2 -2
- package/dist_ts_web/router.js +2 -2
- package/package.json +10 -10
- package/readme.md +42 -20
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cache/classes.cachedb.ts +13 -13
- package/ts/classes.dcrouter.ts +46 -1
- package/ts/plugins.ts +4 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +98 -63
- package/ts_web/elements/ops-view-certificates.ts +1 -1
- package/ts_web/elements/ops-view-emails.ts +1 -1
- package/ts_web/elements/ops-view-network.ts +21 -6
- package/ts_web/elements/ops-view-remoteingress.ts +1 -1
- package/ts_web/readme.md +11 -1
- package/ts_web/router.ts +1 -1
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -528,10 +528,36 @@ export class DcRouter {
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
public async start() {
|
|
531
|
+
await this.checkSystemLimits();
|
|
531
532
|
logger.log('info', 'Starting DcRouter Services');
|
|
532
533
|
await this.serviceManager.start();
|
|
533
534
|
this.logStartupSummary();
|
|
534
535
|
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Detect OS-level resource limits and warn if they are too low for production use.
|
|
539
|
+
* This is detection only — no attempts to raise limits.
|
|
540
|
+
*/
|
|
541
|
+
private async checkSystemLimits(): Promise<void> {
|
|
542
|
+
try {
|
|
543
|
+
const fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
|
544
|
+
const limitsContent = await fs.file('/proc/self/limits').encoding('utf8').read() as string;
|
|
545
|
+
const nofileLine = limitsContent.split('\n').find((line: string) => line.startsWith('Max open files'));
|
|
546
|
+
if (nofileLine) {
|
|
547
|
+
const parts = nofileLine.split(/\s{2,}/);
|
|
548
|
+
const softLimit = parseInt(parts[1], 10);
|
|
549
|
+
const hardLimit = parseInt(parts[2], 10);
|
|
550
|
+
if (softLimit < 65536) {
|
|
551
|
+
logger.log('warn', `File descriptor soft limit is ${softLimit} (hard: ${hardLimit}). ` +
|
|
552
|
+
`For production use, set --ulimit nofile=65536:65536 on the container runtime.`);
|
|
553
|
+
} else {
|
|
554
|
+
logger.log('info', `File descriptor limits: soft=${softLimit}, hard=${hardLimit}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
// Non-Linux or /proc not available — silently skip
|
|
559
|
+
}
|
|
560
|
+
}
|
|
535
561
|
|
|
536
562
|
/**
|
|
537
563
|
* Log comprehensive startup summary
|
|
@@ -708,9 +734,28 @@ export class DcRouter {
|
|
|
708
734
|
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
|
709
735
|
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
|
710
736
|
|
|
711
|
-
// Create SmartProxy configuration
|
|
737
|
+
// Create SmartProxy configuration with sensible gateway defaults.
|
|
738
|
+
// User's smartProxyConfig overrides these defaults via spread.
|
|
712
739
|
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
|
740
|
+
// --- dcrouter gateway defaults ---
|
|
741
|
+
maxConnectionsPerIP: 100,
|
|
742
|
+
connectionRateLimitPerMinute: 600,
|
|
743
|
+
socketTimeout: 120_000,
|
|
744
|
+
inactivityTimeout: 120_000,
|
|
745
|
+
keepAlive: true,
|
|
746
|
+
noDelay: true,
|
|
747
|
+
gracefulShutdownTimeout: 30_000,
|
|
748
|
+
// --- user overrides ---
|
|
713
749
|
...this.options.smartProxyConfig,
|
|
750
|
+
// --- deep-merge defaults.security so user can override maxConnections ---
|
|
751
|
+
defaults: {
|
|
752
|
+
...this.options.smartProxyConfig?.defaults,
|
|
753
|
+
security: {
|
|
754
|
+
maxConnections: 50_000,
|
|
755
|
+
...this.options.smartProxyConfig?.defaults?.security,
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
// --- always set by dcrouter (after spread) ---
|
|
714
759
|
routes,
|
|
715
760
|
acme: acmeConfig,
|
|
716
761
|
certStore: {
|
package/ts/plugins.ts
CHANGED
|
@@ -47,13 +47,13 @@ import * as qenv from '@push.rocks/qenv';
|
|
|
47
47
|
import * as smartacme from '@push.rocks/smartacme';
|
|
48
48
|
import * as smartdata from '@push.rocks/smartdata';
|
|
49
49
|
import * as smartdns from '@push.rocks/smartdns';
|
|
50
|
-
import * as
|
|
50
|
+
import * as smartfs from '@push.rocks/smartfs';
|
|
51
51
|
import * as smartguard from '@push.rocks/smartguard';
|
|
52
52
|
import * as smartjwt from '@push.rocks/smartjwt';
|
|
53
53
|
import * as smartlog from '@push.rocks/smartlog';
|
|
54
54
|
import * as smartmetrics from '@push.rocks/smartmetrics';
|
|
55
55
|
import * as smartmta from '@push.rocks/smartmta';
|
|
56
|
-
import * as
|
|
56
|
+
import * as smartdb from '@push.rocks/smartdb';
|
|
57
57
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
|
58
58
|
import * as smartpath from '@push.rocks/smartpath';
|
|
59
59
|
import * as smartproxy from '@push.rocks/smartproxy';
|
|
@@ -64,7 +64,7 @@ import * as smartrx from '@push.rocks/smartrx';
|
|
|
64
64
|
import * as smartunique from '@push.rocks/smartunique';
|
|
65
65
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
|
66
66
|
|
|
67
|
-
export { projectinfo, qenv, smartacme, smartdata, smartdns,
|
|
67
|
+
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, taskbuffer };
|
|
68
68
|
|
|
69
69
|
// Define SmartLog types for use in error handling
|
|
70
70
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
|
@@ -90,7 +90,7 @@ export {
|
|
|
90
90
|
uuid,
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// Filesystem utilities
|
|
93
|
+
// Filesystem utilities
|
|
94
94
|
export const fsUtils = {
|
|
95
95
|
/**
|
|
96
96
|
* Ensure a directory exists, creating it recursively if needed (sync)
|
package/ts_web/appstate.ts
CHANGED
|
@@ -1186,18 +1186,33 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
|
|
1186
1186
|
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
|
1187
1187
|
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
|
1188
1188
|
|
|
1189
|
+
// Batched log entry handler — buffers incoming entries and flushes once per animation frame
|
|
1190
|
+
let logEntryBuffer: interfaces.data.ILogEntry[] = [];
|
|
1191
|
+
let logFlushScheduled = false;
|
|
1192
|
+
|
|
1193
|
+
function flushLogEntries() {
|
|
1194
|
+
logFlushScheduled = false;
|
|
1195
|
+
if (logEntryBuffer.length === 0) return;
|
|
1196
|
+
const current = logStatePart.getState()!;
|
|
1197
|
+
const updated = [...current.recentLogs, ...logEntryBuffer];
|
|
1198
|
+
logEntryBuffer = [];
|
|
1199
|
+
// Cap at 2000 entries
|
|
1200
|
+
if (updated.length > 2000) {
|
|
1201
|
+
updated.splice(0, updated.length - 2000);
|
|
1202
|
+
}
|
|
1203
|
+
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1189
1206
|
// Register handler for pushed log entries from the server
|
|
1190
1207
|
socketRouter.addTypedHandler(
|
|
1191
1208
|
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
|
|
1192
1209
|
'pushLogEntry',
|
|
1193
1210
|
async (dataArg) => {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
updated.splice(0, updated.length - 2000);
|
|
1211
|
+
logEntryBuffer.push(dataArg.entry);
|
|
1212
|
+
if (!logFlushScheduled) {
|
|
1213
|
+
logFlushScheduled = true;
|
|
1214
|
+
requestAnimationFrame(flushLogEntries);
|
|
1199
1215
|
}
|
|
1200
|
-
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
|
|
1201
1216
|
return {};
|
|
1202
1217
|
}
|
|
1203
1218
|
)
|
|
@@ -1228,8 +1243,21 @@ async function disconnectSocket() {
|
|
|
1228
1243
|
}
|
|
1229
1244
|
}
|
|
1230
1245
|
|
|
1246
|
+
// In-flight guard to prevent concurrent refresh requests
|
|
1247
|
+
let isRefreshing = false;
|
|
1248
|
+
|
|
1231
1249
|
// Combined refresh action for efficient polling
|
|
1232
1250
|
async function dispatchCombinedRefreshAction() {
|
|
1251
|
+
if (isRefreshing) return;
|
|
1252
|
+
isRefreshing = true;
|
|
1253
|
+
try {
|
|
1254
|
+
await dispatchCombinedRefreshActionInner();
|
|
1255
|
+
} finally {
|
|
1256
|
+
isRefreshing = false;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async function dispatchCombinedRefreshActionInner() {
|
|
1233
1261
|
const context = getActionContext();
|
|
1234
1262
|
if (!context.identity) return;
|
|
1235
1263
|
const currentView = uiStatePart.getState()!.activeView;
|
|
@@ -1355,48 +1383,48 @@ async function dispatchCombinedRefreshAction() {
|
|
|
1355
1383
|
}
|
|
1356
1384
|
}
|
|
1357
1385
|
|
|
1358
|
-
//
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
// Only start if conditions are met and not already running at the same rate
|
|
1369
|
-
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
|
1370
|
-
// Check if we need to restart the interval (rate changed or not running)
|
|
1371
|
-
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
|
|
1372
|
-
stopAutoRefresh();
|
|
1373
|
-
currentRefreshRate = uiState.refreshInterval;
|
|
1374
|
-
refreshInterval = setInterval(() => {
|
|
1375
|
-
// Use combined refresh action for efficiency
|
|
1376
|
-
dispatchCombinedRefreshAction();
|
|
1377
|
-
}, uiState.refreshInterval);
|
|
1378
|
-
}
|
|
1379
|
-
} else {
|
|
1380
|
-
stopAutoRefresh();
|
|
1381
|
-
}
|
|
1382
|
-
};
|
|
1386
|
+
// Create a proper action for the combined refresh so we can use createScheduledAction
|
|
1387
|
+
const combinedRefreshAction = statsStatePart.createAction<void>(async (statePartArg) => {
|
|
1388
|
+
await dispatchCombinedRefreshAction();
|
|
1389
|
+
// Return current state — dispatchCombinedRefreshAction already updates all state parts directly
|
|
1390
|
+
return statePartArg.getState()!;
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Scheduled refresh process with autoPause: 'visibility' — automatically pauses when tab is hidden
|
|
1394
|
+
let refreshProcess: ReturnType<typeof statsStatePart.createScheduledAction> | null = null;
|
|
1383
1395
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1396
|
+
const startAutoRefresh = () => {
|
|
1397
|
+
const uiState = uiStatePart.getState()!;
|
|
1398
|
+
const loginState = loginStatePart.getState()!;
|
|
1399
|
+
|
|
1400
|
+
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
|
1401
|
+
// Dispose old process if interval changed or not running
|
|
1402
|
+
if (refreshProcess) {
|
|
1403
|
+
refreshProcess.dispose();
|
|
1404
|
+
refreshProcess = null;
|
|
1389
1405
|
}
|
|
1390
|
-
|
|
1406
|
+
refreshProcess = statsStatePart.createScheduledAction({
|
|
1407
|
+
action: combinedRefreshAction,
|
|
1408
|
+
payload: undefined,
|
|
1409
|
+
intervalMs: uiState.refreshInterval,
|
|
1410
|
+
autoPause: 'visibility',
|
|
1411
|
+
});
|
|
1412
|
+
} else {
|
|
1413
|
+
if (refreshProcess) {
|
|
1414
|
+
refreshProcess.dispose();
|
|
1415
|
+
refreshProcess = null;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1391
1419
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (state.autoRefresh !== previousAutoRefresh ||
|
|
1420
|
+
// Watch for relevant changes
|
|
1421
|
+
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
|
|
1422
|
+
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
|
|
1423
|
+
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
|
|
1424
|
+
|
|
1425
|
+
uiStatePart.select((s) => ({ autoRefresh: s.autoRefresh, refreshInterval: s.refreshInterval }))
|
|
1426
|
+
.subscribe((state) => {
|
|
1427
|
+
if (state.autoRefresh !== previousAutoRefresh ||
|
|
1400
1428
|
state.refreshInterval !== previousRefreshInterval) {
|
|
1401
1429
|
previousAutoRefresh = state.autoRefresh;
|
|
1402
1430
|
previousRefreshInterval = state.refreshInterval;
|
|
@@ -1404,26 +1432,33 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
|
|
1404
1432
|
}
|
|
1405
1433
|
});
|
|
1406
1434
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
startAutoRefresh();
|
|
1435
|
+
loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
|
|
1436
|
+
if (isLoggedIn !== previousIsLoggedIn) {
|
|
1437
|
+
previousIsLoggedIn = isLoggedIn;
|
|
1438
|
+
startAutoRefresh();
|
|
1412
1439
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
}
|
|
1440
|
+
// Connect/disconnect TypedSocket based on login state
|
|
1441
|
+
if (isLoggedIn) {
|
|
1442
|
+
connectSocket();
|
|
1443
|
+
} else {
|
|
1444
|
+
disconnectSocket();
|
|
1419
1445
|
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
// Initial start
|
|
1423
|
-
startAutoRefresh();
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1424
1448
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1449
|
+
// Pause/resume WebSocket when tab visibility changes
|
|
1450
|
+
document.addEventListener('visibilitychange', () => {
|
|
1451
|
+
if (document.hidden) {
|
|
1452
|
+
disconnectSocket();
|
|
1453
|
+
} else if (loginStatePart.getState()!.isLoggedIn) {
|
|
1427
1454
|
connectSocket();
|
|
1428
1455
|
}
|
|
1429
|
-
})
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
// Initial start
|
|
1459
|
+
startAutoRefresh();
|
|
1460
|
+
|
|
1461
|
+
// Connect TypedSocket if already logged in (e.g., persistent session)
|
|
1462
|
+
if (loginStatePart.getState()!.isLoggedIn) {
|
|
1463
|
+
connectSocket();
|
|
1464
|
+
}
|
|
@@ -25,7 +25,7 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
25
25
|
|
|
26
26
|
constructor() {
|
|
27
27
|
super();
|
|
28
|
-
const sub = appstate.certificateStatePart.
|
|
28
|
+
const sub = appstate.certificateStatePart.select().subscribe((newState) => {
|
|
29
29
|
this.certState = newState;
|
|
30
30
|
});
|
|
31
31
|
this.rxSubscriptions.push(sub);
|
|
@@ -28,7 +28,7 @@ export class OpsViewEmails extends DeesElement {
|
|
|
28
28
|
|
|
29
29
|
async connectedCallback() {
|
|
30
30
|
await super.connectedCallback();
|
|
31
|
-
this.stateSubscription = appstate.emailOpsStatePart.
|
|
31
|
+
this.stateSubscription = appstate.emailOpsStatePart.select().subscribe((state) => {
|
|
32
32
|
this.emails = state.emails;
|
|
33
33
|
this.isLoading = state.isLoading;
|
|
34
34
|
});
|
|
@@ -47,10 +47,11 @@ export class OpsViewNetwork extends DeesElement {
|
|
|
47
47
|
// Track if we need to update the chart to avoid unnecessary re-renders
|
|
48
48
|
private lastChartUpdate = 0;
|
|
49
49
|
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
private trafficUpdateTimer: any = null;
|
|
52
52
|
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
|
53
53
|
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
|
54
|
+
private visibilityHandler: (() => void) | null = null;
|
|
54
55
|
|
|
55
56
|
constructor() {
|
|
56
57
|
super();
|
|
@@ -59,28 +60,42 @@ export class OpsViewNetwork extends DeesElement {
|
|
|
59
60
|
this.updateNetworkData();
|
|
60
61
|
this.startTrafficUpdateTimer();
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
async connectedCallback() {
|
|
64
65
|
await super.connectedCallback();
|
|
65
|
-
|
|
66
|
+
|
|
67
|
+
// Pause/resume traffic timer when tab visibility changes
|
|
68
|
+
this.visibilityHandler = () => {
|
|
69
|
+
if (document.hidden) {
|
|
70
|
+
this.stopTrafficUpdateTimer();
|
|
71
|
+
} else {
|
|
72
|
+
this.startTrafficUpdateTimer();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
document.addEventListener('visibilitychange', this.visibilityHandler);
|
|
76
|
+
|
|
66
77
|
// When network view becomes visible, ensure we fetch network data
|
|
67
78
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
|
68
79
|
}
|
|
69
|
-
|
|
80
|
+
|
|
70
81
|
async disconnectedCallback() {
|
|
71
82
|
await super.disconnectedCallback();
|
|
72
83
|
this.stopTrafficUpdateTimer();
|
|
84
|
+
if (this.visibilityHandler) {
|
|
85
|
+
document.removeEventListener('visibilitychange', this.visibilityHandler);
|
|
86
|
+
this.visibilityHandler = null;
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
private subscribeToStateParts() {
|
|
76
91
|
// Subscribe and track unsubscribe functions
|
|
77
|
-
const statsUnsubscribe = appstate.statsStatePart.
|
|
92
|
+
const statsUnsubscribe = appstate.statsStatePart.select().subscribe((state) => {
|
|
78
93
|
this.statsState = state;
|
|
79
94
|
this.updateNetworkData();
|
|
80
95
|
});
|
|
81
96
|
this.rxSubscriptions.push(statsUnsubscribe);
|
|
82
97
|
|
|
83
|
-
const networkUnsubscribe = appstate.networkStatePart.
|
|
98
|
+
const networkUnsubscribe = appstate.networkStatePart.select().subscribe((state) => {
|
|
84
99
|
this.networkState = state;
|
|
85
100
|
this.updateNetworkData();
|
|
86
101
|
});
|
|
@@ -25,7 +25,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
25
25
|
|
|
26
26
|
constructor() {
|
|
27
27
|
super();
|
|
28
|
-
const sub = appstate.remoteIngressStatePart.
|
|
28
|
+
const sub = appstate.remoteIngressStatePart.select().subscribe((newState) => {
|
|
29
29
|
this.riState = newState;
|
|
30
30
|
});
|
|
31
31
|
this.rxSubscriptions.push(sub);
|
package/ts_web/readme.md
CHANGED
|
@@ -111,7 +111,7 @@ ts_web/
|
|
|
111
111
|
|
|
112
112
|
### State Management
|
|
113
113
|
|
|
114
|
-
The app uses `@push.rocks/smartstate` with multiple state parts:
|
|
114
|
+
The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled actions with `autoPause: 'visibility'`, and batched updates:
|
|
115
115
|
|
|
116
116
|
| State Part | Mode | Description |
|
|
117
117
|
|-----------|------|-------------|
|
|
@@ -125,6 +125,16 @@ The app uses `@push.rocks/smartstate` with multiple state parts:
|
|
|
125
125
|
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
|
|
126
126
|
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
|
|
127
127
|
|
|
128
|
+
### Tab Visibility Optimization
|
|
129
|
+
|
|
130
|
+
The dashboard automatically pauses all background activity when the browser tab is hidden and resumes when visible:
|
|
131
|
+
|
|
132
|
+
- **Auto-refresh polling** uses `createScheduledAction` with `autoPause: 'visibility'` — stops HTTP requests while the tab is sleeping
|
|
133
|
+
- **In-flight guard** prevents concurrent refresh requests from piling up
|
|
134
|
+
- **WebSocket connection** disconnects when hidden and reconnects when visible, preventing log entry accumulation
|
|
135
|
+
- **Network traffic timer** pauses chart updates when the tab is backgrounded
|
|
136
|
+
- **Log entry batching** — incoming WebSocket log pushes are buffered and flushed once per animation frame to avoid per-entry re-renders
|
|
137
|
+
|
|
128
138
|
### Actions
|
|
129
139
|
|
|
130
140
|
```typescript
|
package/ts_web/router.ts
CHANGED
|
@@ -38,7 +38,7 @@ class AppRouter {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
private setupStateSync(): void {
|
|
41
|
-
appstate.uiStatePart.
|
|
41
|
+
appstate.uiStatePart.select().subscribe((uiState) => {
|
|
42
42
|
if (this.suppressStateUpdate) return;
|
|
43
43
|
|
|
44
44
|
const currentPath = window.location.pathname;
|