Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d443fa147 | |||
| 2efdd2f16b | |||
| ec0348a83c | |||
| 6c4adf70c7 |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-27 - 11.12.1 - fix(dcrouter)
|
||||||
|
retry auto certificate provisioning after SmartAcme becomes ready
|
||||||
|
|
||||||
|
- detects certificates that failed during startup before the DNS-01 provider was available
|
||||||
|
- clears provisioning backoff and failed status for affected domains before retrying
|
||||||
|
- re-triggers auto certificate provisioning for SmartProxy routes once SmartAcme is ready
|
||||||
|
|
||||||
|
## 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)
|
## 2026-03-26 - 11.11.0 - feat(docker,cache,proxy)
|
||||||
improve container runtime defaults and add configurable connection limits
|
improve container runtime defaults and add configurable connection limits
|
||||||
|
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.11.0",
|
"version": "11.12.1",
|
||||||
"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": {
|
||||||
@@ -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,13 +36,13 @@
|
|||||||
"@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": "^1.0.1",
|
"@push.rocks/smartdb": "^2.0.0",
|
||||||
"@push.rocks/smartdns": "^7.9.0",
|
"@push.rocks/smartdns": "^7.9.0",
|
||||||
"@push.rocks/smartfs": "^1.5.0",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
@@ -53,16 +53,16 @@
|
|||||||
"@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": "^26.3.0",
|
"@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"
|
||||||
|
|||||||
429
pnpm-lock.yaml
generated
429
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.11.0',
|
version: '11.12.1',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,6 +388,36 @@ export class DcRouter {
|
|||||||
await this.smartAcme.start();
|
await this.smartAcme.start();
|
||||||
this.smartAcmeReady = true;
|
this.smartAcmeReady = true;
|
||||||
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
||||||
|
|
||||||
|
// Re-provision any certificates that failed during the startup window
|
||||||
|
// (before SmartAcme was ready — the certProvisionFunction returned 'http01'
|
||||||
|
// which fails because Rust ACME is disabled when certProvisionFunction is set)
|
||||||
|
if (this.smartProxy) {
|
||||||
|
const failedDomains = [...this.certificateStatusMap.entries()]
|
||||||
|
.filter(([_, status]) => status.status === 'failed')
|
||||||
|
.map(([domain]) => domain);
|
||||||
|
|
||||||
|
if (failedDomains.length > 0) {
|
||||||
|
logger.log('info', `Re-provisioning ${failedDomains.length} certificates that failed before SmartAcme was ready`);
|
||||||
|
// Clear backoff and status for failed domains — these failures were from the startup race
|
||||||
|
for (const domain of failedDomains) {
|
||||||
|
if (this.certProvisionScheduler) {
|
||||||
|
await this.certProvisionScheduler.clearBackoff(domain);
|
||||||
|
}
|
||||||
|
this.certificateStatusMap.delete(domain);
|
||||||
|
}
|
||||||
|
// Re-trigger provisioning for all auto-cert routes
|
||||||
|
const routes = this.smartProxy.routeManager.getRoutes();
|
||||||
|
for (const route of routes) {
|
||||||
|
const tls = (route as any).action?.tls;
|
||||||
|
if (tls && tls.certificate === 'auto') {
|
||||||
|
this.smartProxy.provisionCertificate(route.name).catch((err: any) => {
|
||||||
|
logger.log('warn', `Re-provision for route '${route.name}' failed: ${err?.message || err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.withStop(async () => {
|
.withStop(async () => {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.11.0',
|
version: '11.12.1',
|
||||||
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,47 +1383,47 @@ 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;
|
||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
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();
|
||||||
@@ -63,6 +64,16 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -70,17 +81,21 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
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