This commit is contained in:
Juergen Kunz
2025-07-02 11:33:50 +00:00
parent 7bd94884f4
commit 2f46b3c9f3
9 changed files with 688 additions and 300 deletions

View File

@@ -89,7 +89,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
export const uiStatePart = await appState.getStatePart<IUiState>(
'ui',
{
activeView: 'dashboard',
activeView: 'overview',
sidebarCollapsed: false,
autoRefresh: true,
refreshInterval: 1000, // 1 second
@@ -184,56 +184,35 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
};
});
// Fetch All Stats Action
// Fetch All Stats Action - Using combined endpoint for efficiency
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
// Fetch server stats
const serverStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetServerStatistics
>('/typedrequest', 'getServerStatistics');
// Use combined metrics endpoint - single request instead of 4
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetCombinedMetrics
>('/typedrequest', 'getCombinedMetrics');
const serverStatsResponse = await serverStatsRequest.fire({
const combinedResponse = await combinedRequest.fire({
identity: context.identity,
includeHistory: false,
sections: {
server: true,
email: true,
dns: true,
security: true,
network: false, // Network is fetched separately for the network view
},
});
// Fetch email stats
const emailStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetEmailStatistics
>('/typedrequest', 'getEmailStatistics');
const emailStatsResponse = await emailStatsRequest.fire({
identity: context.identity,
});
// Fetch DNS stats
const dnsStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetDnsStatistics
>('/typedrequest', 'getDnsStatistics');
const dnsStatsResponse = await dnsStatsRequest.fire({
identity: context.identity,
});
// Fetch security metrics
const securityRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetSecurityMetrics
>('/typedrequest', 'getSecurityMetrics');
const securityResponse = await securityRequest.fire({
identity: context.identity,
});
// Update state with all stats
// Update state with all stats from combined response
return {
serverStats: serverStatsResponse.stats,
emailStats: emailStatsResponse.stats,
dnsStats: dnsStatsResponse.stats,
securityMetrics: securityResponse.metrics,
serverStats: combinedResponse.metrics.server || currentState.serverStats,
emailStats: combinedResponse.metrics.email || currentState.emailStats,
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
lastUpdated: Date.now(),
isLoading: false,
error: null,
@@ -342,6 +321,14 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
// Set Active View Action
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
const currentState = statePartArg.getState();
// If switching to network view, ensure we fetch network data
if (viewName === 'network' && currentState.activeView !== 'network') {
setTimeout(() => {
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
}, 100);
}
return {
...currentState,
activeView: viewName,
@@ -410,18 +397,118 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
}
});
// Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() {
const context = getActionContext();
const currentView = uiStatePart.getState().activeView;
try {
// Always fetch basic stats for dashboard widgets
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetCombinedMetrics
>('/typedrequest', 'getCombinedMetrics');
const combinedResponse = await combinedRequest.fire({
identity: context.identity,
sections: {
server: true,
email: true,
dns: true,
security: true,
network: currentView === 'network' || currentView === 'Network', // Only fetch network if on network view
},
});
// Update all stats from combined response
statsStatePart.setState({
...statsStatePart.getState(),
serverStats: combinedResponse.metrics.server || statsStatePart.getState().serverStats,
emailStats: combinedResponse.metrics.email || statsStatePart.getState().emailStats,
dnsStats: combinedResponse.metrics.dns || statsStatePart.getState().dnsStats,
securityMetrics: combinedResponse.metrics.security || statsStatePart.getState().securityMetrics,
lastUpdated: Date.now(),
isLoading: false,
error: null,
});
// Update network stats if included
if (combinedResponse.metrics.network && (currentView === 'network' || currentView === 'Network')) {
const network = combinedResponse.metrics.network;
const connectionsByIP: { [ip: string]: number } = {};
// Convert connection details to IP counts
network.connectionDetails.forEach(conn => {
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
});
// Fetch detailed connections for the network view
try {
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetActiveConnections
>('/typedrequest', 'getActiveConnections');
const connectionsResponse = await connectionsRequest.fire({
identity: context.identity,
});
networkStatePart.setState({
...networkStatePart.getState(),
connections: connectionsResponse.connections,
connectionsByIP,
throughputRate: {
bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out
},
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
lastUpdated: Date.now(),
isLoading: false,
error: null,
});
} catch (error) {
console.error('Failed to fetch connections:', error);
networkStatePart.setState({
...networkStatePart.getState(),
connections: [],
connectionsByIP,
throughputRate: {
bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out
},
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
lastUpdated: Date.now(),
isLoading: false,
error: null,
});
}
}
} catch (error) {
console.error('Combined refresh failed:', error);
}
}
// Initialize auto-refresh
let refreshInterval: NodeJS.Timeout | null = null;
let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts
// Initialize auto-refresh when UI state is ready
(() => {
const startAutoRefresh = () => {
const uiState = uiStatePart.getState();
if (uiState.autoRefresh && loginStatePart.getState().isLoggedIn) {
refreshInterval = setInterval(() => {
statsStatePart.dispatchAction(fetchAllStatsAction, null);
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
}, uiState.refreshInterval);
const loginState = loginStatePart.getState();
// Only start if conditions are met and not already running at the same rate
if (uiState.autoRefresh && loginState.isLoggedIn) {
// Check if we need to restart the interval (rate changed or not running)
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
stopAutoRefresh();
currentRefreshRate = uiState.refreshInterval;
refreshInterval = setInterval(() => {
// Use combined refresh action for efficiency
dispatchCombinedRefreshAction();
}, uiState.refreshInterval);
}
} else {
stopAutoRefresh();
}
};
@@ -429,18 +516,31 @@ let refreshInterval: NodeJS.Timeout | null = null;
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
currentRefreshRate = 0;
}
};
// Watch for changes
uiStatePart.state.subscribe(() => {
stopAutoRefresh();
startAutoRefresh();
// Watch for relevant changes only
let previousAutoRefresh = uiStatePart.getState().autoRefresh;
let previousRefreshInterval = uiStatePart.getState().refreshInterval;
let previousIsLoggedIn = loginStatePart.getState().isLoggedIn;
uiStatePart.state.subscribe((state) => {
// Only restart if relevant values changed
if (state.autoRefresh !== previousAutoRefresh ||
state.refreshInterval !== previousRefreshInterval) {
previousAutoRefresh = state.autoRefresh;
previousRefreshInterval = state.refreshInterval;
startAutoRefresh();
}
});
loginStatePart.state.subscribe(() => {
stopAutoRefresh();
startAutoRefresh();
loginStatePart.state.subscribe((state) => {
// Only restart if login state changed
if (state.isLoggedIn !== previousIsLoggedIn) {
previousIsLoggedIn = state.isLoggedIn;
startAutoRefresh();
}
});
// Initial start

View File

@@ -127,6 +127,16 @@ export class OpsDashboard extends DeesElement {
this.login(e.detail.data.username, e.detail.data.password);
});
// Handle view changes
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
if (appDash) {
appDash.addEventListener('viewSwitch', (e: CustomEvent) => {
const viewName = e.detail.tabName;
console.log('View switched to:', viewName);
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase());
});
}
// Handle initial state
const loginState = appstate.loginStatePart.getState();
console.log('Initial login state:', loginState);

View File

@@ -43,6 +43,10 @@ export class OpsViewNetwork extends DeesElement {
@state()
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
// Track if we need to update the chart to avoid unnecessary re-renders
private lastChartUpdate = 0;
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
private lastTrafficUpdateTime = 0;
private trafficUpdateInterval = 1000; // Update every 1 second
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
@@ -59,21 +63,35 @@ export class OpsViewNetwork extends DeesElement {
this.startTrafficUpdateTimer();
}
async connectedCallback() {
await super.connectedCallback();
// When network view becomes visible, ensure we fetch network data
console.log('Network view connected - fetching initial data');
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
// Also update the active view state
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, 'network');
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.stopTrafficUpdateTimer();
}
private subscribeToStateParts() {
appstate.statsStatePart.state.subscribe((state) => {
// Subscribe and track unsubscribe functions
const statsUnsubscribe = appstate.statsStatePart.state.subscribe((state) => {
this.statsState = state;
this.updateNetworkData();
});
this.rxSubscriptions.push(statsUnsubscribe);
appstate.networkStatePart.state.subscribe((state) => {
const networkUnsubscribe = appstate.networkStatePart.state.subscribe((state) => {
this.networkState = state;
this.updateNetworkData();
});
this.rxSubscriptions.push(networkUnsubscribe);
}
private initializeTrafficData() {
@@ -169,6 +187,13 @@ export class OpsViewNetwork extends DeesElement {
];
public render() {
console.log('Network view render - chart data points:', {
inPoints: this.trafficDataIn.length,
outPoints: this.trafficDataOut.length,
lastInValue: this.trafficDataIn[this.trafficDataIn.length - 1]?.y,
lastOutValue: this.trafficDataOut[this.trafficDataOut.length - 1]?.y
});
return html`
<ops-sectionheading>Network Activity</ops-sectionheading>
@@ -278,7 +303,6 @@ export class OpsViewNetwork extends DeesElement {
iconName: 'copy',
action: async () => {
await navigator.clipboard.writeText(request.id);
console.log('Request ID copied to clipboard');
}
}
]
@@ -366,6 +390,8 @@ export class OpsViewNetwork extends DeesElement {
const reqPerSec = this.calculateRequestsPerSecond();
const throughput = this.calculateThroughput();
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
// Throughput data is now available in the stats tiles
// Use request count history for the requests/sec trend
const trendData = [...this.requestsPerSecHistory];
@@ -466,25 +492,36 @@ export class OpsViewNetwork extends DeesElement {
}
private async updateNetworkData() {
// Convert connection data to network requests format
if (this.networkState.connections.length > 0) {
this.networkRequests = this.networkState.connections.map((conn, index) => ({
id: conn.id,
timestamp: conn.startTime,
method: 'GET', // Default method for proxy connections
url: '/',
hostname: conn.remoteAddress,
port: conn.protocol === 'https' ? 443 : 80,
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
statusCode: conn.state === 'connected' ? 200 : undefined,
duration: Date.now() - conn.startTime,
bytesIn: conn.bytesReceived,
bytesOut: conn.bytesSent,
remoteIp: conn.remoteAddress,
route: 'proxy',
}));
} else {
this.networkRequests = [];
// Only update if connections changed significantly
const newConnectionCount = this.networkState.connections.length;
const oldConnectionCount = this.networkRequests.length;
// Check if we need to update the network requests array
const shouldUpdate = newConnectionCount !== oldConnectionCount ||
newConnectionCount === 0 ||
(newConnectionCount > 0 && this.networkRequests.length === 0);
if (shouldUpdate) {
// Convert connection data to network requests format
if (newConnectionCount > 0) {
this.networkRequests = this.networkState.connections.map((conn, index) => ({
id: conn.id,
timestamp: conn.startTime,
method: 'GET', // Default method for proxy connections
url: '/',
hostname: conn.remoteAddress,
port: conn.protocol === 'https' ? 443 : 80,
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
statusCode: conn.state === 'connected' ? 200 : undefined,
duration: Date.now() - conn.startTime,
bytesIn: conn.bytesReceived,
bytesOut: conn.bytesSent,
remoteIp: conn.remoteAddress,
route: 'proxy',
}));
} else {
this.networkRequests = [];
}
}
// Generate traffic data based on request history
@@ -492,87 +529,58 @@ export class OpsViewNetwork extends DeesElement {
}
private updateTrafficData() {
// This method is called when network data updates
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
console.log('UpdateTrafficData called - network data updated');
}
private startTrafficUpdateTimer() {
this.stopTrafficUpdateTimer(); // Clear any existing timer
this.trafficUpdateTimer = setInterval(() => {
// Add a new data point every second
this.addTrafficDataPoint();
}, 1000); // Update every second
}
private addTrafficDataPoint() {
const now = Date.now();
// Fixed 5 minute time range
const range = 5 * 60 * 1000; // 5 minutes
const bucketSize = range / 60; // 60 data points // 60 data points
// Check if enough time has passed to add a new data point
const timeSinceLastUpdate = now - this.lastTrafficUpdateTime;
const shouldAddNewPoint = timeSinceLastUpdate >= this.trafficUpdateInterval;
console.log('UpdateTrafficData called:', {
networkRequestsCount: this.networkRequests.length,
timeSinceLastUpdate,
shouldAddNewPoint,
currentDataPoints: this.trafficDataIn.length
});
if (!shouldAddNewPoint && this.trafficDataIn.length > 0) {
// Not enough time has passed, don't update
// Throttle chart updates to avoid excessive re-renders
if (now - this.lastChartUpdate < this.chartUpdateThreshold) {
return;
}
// Use real-time throughput data from SmartProxy (same as throughput tiles)
const throughput = this.calculateThroughput();
// Convert to Mbps (bytes * 8 / 1,000,000)
const throughputInMbps = (throughput.in * 8) / 1000000;
const throughputOutMbps = (throughput.out * 8) / 1000000;
console.log('Throughput calculation:', {
bytesInPerSecond: throughput.in,
bytesOutPerSecond: throughput.out,
throughputInMbps,
throughputOutMbps,
throughputTileValue: `${this.formatBitsPerSecond(throughput.in)} IN, ${this.formatBitsPerSecond(throughput.out)} OUT`
});
// Add new data points
const timestamp = new Date(now).toISOString();
if (this.trafficDataIn.length === 0) {
// Initialize if empty
this.initializeTrafficData();
const newDataPointIn = {
x: timestamp,
y: Math.round(throughputInMbps * 10) / 10
};
const newDataPointOut = {
x: timestamp,
y: Math.round(throughputOutMbps * 10) / 10
};
// Efficient array updates - modify in place when possible
if (this.trafficDataIn.length >= 60) {
// Remove oldest and add newest
this.trafficDataIn = [...this.trafficDataIn.slice(1), newDataPointIn];
this.trafficDataOut = [...this.trafficDataOut.slice(1), newDataPointOut];
} else {
// Add new data points for both in and out
const timestamp = new Date(now).toISOString();
const newDataPointIn = {
x: timestamp,
y: Math.round(throughputInMbps * 10) / 10 // Round to 1 decimal place
};
const newDataPointOut = {
x: timestamp,
y: Math.round(throughputOutMbps * 10) / 10 // Round to 1 decimal place
};
// Create new arrays with existing data plus new points
const newTrafficDataIn = [...this.trafficDataIn, newDataPointIn];
const newTrafficDataOut = [...this.trafficDataOut, newDataPointOut];
// Keep only the last 60 points
if (newTrafficDataIn.length > 60) {
newTrafficDataIn.shift(); // Remove oldest point
newTrafficDataOut.shift();
}
this.trafficDataIn = newTrafficDataIn;
this.trafficDataOut = newTrafficDataOut;
this.lastTrafficUpdateTime = now;
console.log('Added new traffic data points:', {
timestamp: timestamp,
throughputInMbps: newDataPointIn.y,
throughputOutMbps: newDataPointOut.y,
totalPoints: this.trafficDataIn.length
});
// Still filling up the initial data
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
}
}
private startTrafficUpdateTimer() {
this.stopTrafficUpdateTimer(); // Clear any existing timer
this.trafficUpdateTimer = setInterval(() => {
this.updateTrafficData();
}, 1000); // Check every second, but only update when interval has passed
this.lastChartUpdate = now;
}
private stopTrafficUpdateTimer() {
@@ -581,5 +589,4 @@ export class OpsViewNetwork extends DeesElement {
this.trafficUpdateTimer = null;
}
}
}