This commit is contained in:
@@ -107,11 +107,7 @@ function bindLivePeer(conn) {
|
||||
if (conn.capability) entry.capability = conn.capability;
|
||||
updateConnectionStatus(topicId, true);
|
||||
updateConnectionDisplay(topicId);
|
||||
// Restart per-peer health UI loop (clears itself if peer drops)
|
||||
if (entry.healthCheckInterval) {
|
||||
clearInterval(entry.healthCheckInterval);
|
||||
entry.healthCheckInterval = null;
|
||||
}
|
||||
// Sync health UI from live peer (manager health loop owns pings — no second interval)
|
||||
startHealthMonitoring(topicId);
|
||||
}
|
||||
|
||||
@@ -407,14 +403,22 @@ const volumesStore = {
|
||||
// Expose volumes store to window for use by other modules
|
||||
window.volumesStore = volumesStore;
|
||||
let lastStatsUpdate = Date.now();
|
||||
/**
|
||||
* Legacy no-ops: live container stats arrive via push:allStats from the server.
|
||||
* Kept as named exports so call sites stay stable without a 500ms wake timer.
|
||||
*/
|
||||
function stopStatsInterval() {
|
||||
if (statsInterval) {
|
||||
clearInterval(statsInterval);
|
||||
statsInterval = null;
|
||||
console.log('[INFO] Stats interval stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
function startStatsInterval() {
|
||||
// Clear any leftover timer from older sessions; do not schedule a new one.
|
||||
stopStatsInterval();
|
||||
}
|
||||
|
||||
// Utility functions are now imported from uiUtils.js
|
||||
|
||||
|
||||
@@ -428,30 +432,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function startStatsInterval() {
|
||||
// Guard: stop existing interval before starting a new one
|
||||
stopStatsInterval();
|
||||
|
||||
// Only start if there's an active peer
|
||||
if (!window.activePeer) {
|
||||
console.warn('[WARN] No active peer; not starting stats interval.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Increased interval to 500ms for better performance (was 100ms)
|
||||
statsInterval = setInterval(() => {
|
||||
if (window.activePeer) {
|
||||
const now = Date.now();
|
||||
if (now - lastStatsUpdate >= 500) { // Ensure at least 500ms between updates
|
||||
lastStatsUpdate = now;
|
||||
}
|
||||
} else {
|
||||
console.warn('[WARN] No active peer; skipping stats request.');
|
||||
stopStatsInterval(); // Stop interval if peer is no longer active
|
||||
}
|
||||
}, 500); // Poll every 500ms for better performance (reduced from 100ms)
|
||||
}
|
||||
const smoothedStats = {}; // Container-specific smoothing storage
|
||||
const historicalStats = {}; // Container-specific historical stats for charts
|
||||
const MAX_HISTORY_POINTS = 900; // ~30m at 2s broadcast interval
|
||||
@@ -2577,11 +2557,75 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Expose to window for onclick handlers
|
||||
window.navigateToView = navigateToView;
|
||||
|
||||
/**
|
||||
* Dashboard RPC TTL cache — avoids re-hitting the Docker socket on every
|
||||
* navigation to the dashboard when nothing has changed.
|
||||
* Invalidated on peer switch and on relevant list/event responses.
|
||||
*/
|
||||
const DASHBOARD_TTL = {
|
||||
systemInfo: 30_000,
|
||||
systemDf: 30_000,
|
||||
containers: 10_000,
|
||||
images: 20_000,
|
||||
networks: 20_000,
|
||||
volumes: 20_000,
|
||||
};
|
||||
|
||||
/** @type {{ peerId: string, at: Record<string, number> }} */
|
||||
const dashboardRpcCache = { peerId: '', at: {} };
|
||||
|
||||
function dashboardCachePeerId() {
|
||||
return manager.active?.id || window.activePeer?.id || '';
|
||||
}
|
||||
|
||||
function touchDashboardCache(key) {
|
||||
const peerId = dashboardCachePeerId();
|
||||
if (!peerId) return;
|
||||
if (dashboardRpcCache.peerId !== peerId) {
|
||||
dashboardRpcCache.peerId = peerId;
|
||||
dashboardRpcCache.at = {};
|
||||
}
|
||||
dashboardRpcCache.at[key] = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {number} [ttlMs]
|
||||
*/
|
||||
function isDashboardCacheFresh(key, ttlMs) {
|
||||
const peerId = dashboardCachePeerId();
|
||||
if (!peerId || dashboardRpcCache.peerId !== peerId) return false;
|
||||
const at = dashboardRpcCache.at[key];
|
||||
if (!at) return false;
|
||||
const ttl = ttlMs ?? DASHBOARD_TTL[key] ?? 15_000;
|
||||
return Date.now() - at < ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|string[]|'all'} [keys]
|
||||
*/
|
||||
function invalidateDashboardCache(keys = 'all') {
|
||||
if (keys === 'all') {
|
||||
dashboardRpcCache.at = {};
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(keys) ? keys : [keys];
|
||||
for (const k of list) delete dashboardRpcCache.at[k];
|
||||
}
|
||||
|
||||
window.invalidateDashboardCache = invalidateDashboardCache;
|
||||
|
||||
// Dashboard Functions
|
||||
function loadDashboard() {
|
||||
if (!hasActiveConnection() && !window.activePeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerId = dashboardCachePeerId();
|
||||
if (peerId && dashboardRpcCache.peerId !== peerId) {
|
||||
dashboardRpcCache.peerId = peerId;
|
||||
dashboardRpcCache.at = {};
|
||||
}
|
||||
|
||||
// Set up volumes subscription early to catch broadcasts
|
||||
if (!volumesStoreSubscription) {
|
||||
@@ -2627,22 +2671,27 @@ function loadDashboard() {
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Load system info
|
||||
sendCommand('getSystemInfo');
|
||||
sendCommand('getSystemDf');
|
||||
|
||||
// Load container stats for counts
|
||||
sendCommand('listContainers');
|
||||
|
||||
// Load images count
|
||||
sendCommand('listImages');
|
||||
|
||||
// Load networks count
|
||||
sendCommand('listNetworks');
|
||||
|
||||
// Load volumes
|
||||
sendCommand('listVolumes');
|
||||
|
||||
// Only re-fetch slices that are stale — same data when fresh, less Docker socket load.
|
||||
// When fresh, rely on event pushes + auto-refresh for updates.
|
||||
if (!isDashboardCacheFresh('systemInfo')) {
|
||||
sendCommand('getSystemInfo');
|
||||
}
|
||||
if (!isDashboardCacheFresh('systemDf')) {
|
||||
sendCommand('getSystemDf');
|
||||
}
|
||||
if (!isDashboardCacheFresh('containers')) {
|
||||
sendCommand('listContainers');
|
||||
}
|
||||
if (!isDashboardCacheFresh('images')) {
|
||||
sendCommand('listImages');
|
||||
}
|
||||
if (!isDashboardCacheFresh('networks')) {
|
||||
sendCommand('listNetworks');
|
||||
}
|
||||
if (!isDashboardCacheFresh('volumes')) {
|
||||
sendCommand('listVolumes');
|
||||
}
|
||||
}
|
||||
|
||||
window.loadDashboard = loadDashboard;
|
||||
@@ -2662,6 +2711,7 @@ function formatBytes(bytes) {
|
||||
|
||||
function refreshSystemDf() {
|
||||
if (!hasActiveConnection()) return;
|
||||
invalidateDashboardCache('systemDf');
|
||||
sendCommand('getSystemDf');
|
||||
}
|
||||
window.refreshSystemDf = refreshSystemDf;
|
||||
@@ -9773,6 +9823,7 @@ function handleRpcMessage(response, conn) {
|
||||
: null;
|
||||
if (rows) {
|
||||
applyContainerSnapshot(rows, topicId, { source: 'rpc' });
|
||||
touchDashboardCache('containers');
|
||||
if (currentView === 'dashboard') {
|
||||
updateDashboardStats(containerFilterState.allContainers, null, null);
|
||||
}
|
||||
@@ -9869,11 +9920,13 @@ function handleRpcMessage(response, conn) {
|
||||
case 'systemInfo':
|
||||
console.log('[INFO] Handling system information...');
|
||||
updateSystemInfo(response.data);
|
||||
touchDashboardCache('systemInfo');
|
||||
break;
|
||||
|
||||
case 'images':
|
||||
console.log('[INFO] Handling images list...');
|
||||
renderImages(response.data);
|
||||
touchDashboardCache('images');
|
||||
// Update dashboard stats if on dashboard view
|
||||
if (currentView === 'dashboard') {
|
||||
updateDashboardStats(null, response.data, null);
|
||||
@@ -9883,6 +9936,7 @@ function handleRpcMessage(response, conn) {
|
||||
case 'networks':
|
||||
console.log('[INFO] Handling networks list...');
|
||||
renderNetworks(response.data);
|
||||
touchDashboardCache('networks');
|
||||
// Update dashboard stats if on dashboard view
|
||||
if (currentView === 'dashboard') {
|
||||
updateDashboardStats(null, null, response.data);
|
||||
@@ -9903,6 +9957,7 @@ function handleRpcMessage(response, conn) {
|
||||
if (volumesToRender !== null) {
|
||||
// Always update store first (this will trigger subscriptions)
|
||||
volumesStore.set(volumesToRender);
|
||||
touchDashboardCache('volumes');
|
||||
// Always render if on volumes view to ensure UI is updated
|
||||
if (currentView === 'volumes') {
|
||||
renderVolumes(volumesToRender);
|
||||
@@ -9945,6 +10000,13 @@ function handleRpcMessage(response, conn) {
|
||||
}
|
||||
|
||||
case 'dockerEvent': {
|
||||
// Fleet-changing events: force dashboard slices to re-fetch on next visit
|
||||
const ev = response.data || response;
|
||||
const t = String(ev?.Type || ev?.type || '').toLowerCase();
|
||||
if (t === 'container') invalidateDashboardCache('containers');
|
||||
else if (t === 'image') invalidateDashboardCache(['images', 'systemDf']);
|
||||
else if (t === 'volume') invalidateDashboardCache(['volumes', 'systemDf']);
|
||||
else if (t === 'network') invalidateDashboardCache('networks');
|
||||
if (window.handleDockerEvent) {
|
||||
window.handleDockerEvent(response.data);
|
||||
}
|
||||
@@ -10015,6 +10077,7 @@ function handleRpcMessage(response, conn) {
|
||||
|
||||
case 'systemDf':
|
||||
renderSystemDf(response.data);
|
||||
touchDashboardCache('systemDf');
|
||||
break;
|
||||
|
||||
case 'containerTop':
|
||||
@@ -11248,8 +11311,12 @@ function updateConnectionDisplay(topicId) {
|
||||
: String(fullKey);
|
||||
}
|
||||
|
||||
// Health monitoring for connections (UI latency badge). Dead-link detection + reconnect
|
||||
// is owned by ConnectionManager's health loop / PearDockConnection.ping().
|
||||
/**
|
||||
* Sync connection-row health UI from the live peer.
|
||||
* Ping / dead-link detection is owned solely by ConnectionManager's health loop
|
||||
* (manager emits 'health' → updateConnectionDisplay). A second interval here used
|
||||
* to double docker.version() load on the server every 10s.
|
||||
*/
|
||||
function startHealthMonitoring(topicId) {
|
||||
const connection = connections[topicId];
|
||||
if (!connection) return;
|
||||
@@ -11258,36 +11325,20 @@ function startHealthMonitoring(topicId) {
|
||||
connection.healthCheckInterval = null;
|
||||
}
|
||||
|
||||
const healthCheckInterval = setInterval(async () => {
|
||||
const entry = connections[topicId];
|
||||
// Prefer manager's live socket if UI map is briefly stale after reconnect
|
||||
const peer = entry?.peer?.connected
|
||||
? entry.peer
|
||||
const peer =
|
||||
connection.peer?.connected
|
||||
? connection.peer
|
||||
: manager.connections.get(topicId)?.connected
|
||||
? manager.connections.get(topicId)
|
||||
: null;
|
||||
if (!peer) {
|
||||
clearInterval(healthCheckInterval);
|
||||
if (entry) entry.healthCheckInterval = null;
|
||||
return;
|
||||
}
|
||||
if (entry && entry.peer !== peer) entry.peer = peer;
|
||||
try {
|
||||
const ms = await peer.ping();
|
||||
entry.latency = ms;
|
||||
entry.lastHealthCheck = Date.now();
|
||||
entry.healthStatus = ms < 5000 ? 'healthy' : 'slow';
|
||||
updateConnectionStatus(topicId, true);
|
||||
updateConnectionDisplay(topicId);
|
||||
} catch (err) {
|
||||
entry.healthStatus = 'unhealthy';
|
||||
updateConnectionStatus(topicId, false);
|
||||
updateConnectionDisplay(topicId);
|
||||
// Do not clear forever — manager will disconnect + redial; bindLivePeer restarts this loop
|
||||
}
|
||||
}, 10000);
|
||||
if (!peer) return;
|
||||
if (connection.peer !== peer) connection.peer = peer;
|
||||
|
||||
connections[topicId].healthCheckInterval = healthCheckInterval;
|
||||
connection.latency = peer.latency ?? connection.latency;
|
||||
connection.healthStatus = peer.healthStatus || connection.healthStatus || 'healthy';
|
||||
connection.lastHealthCheck = peer.lastHealthCheck || connection.lastHealthCheck || Date.now();
|
||||
updateConnectionStatus(topicId, true);
|
||||
updateConnectionDisplay(topicId);
|
||||
}
|
||||
|
||||
// Switch between connections
|
||||
@@ -11336,6 +11387,7 @@ function switchConnection(topicId, opts = {}) {
|
||||
// Drop previous host's containers immediately so the table never mixes peers
|
||||
resetContainerList();
|
||||
containerStore.topicId = manager.active?.id || topicId || '';
|
||||
invalidateDashboardCache('all');
|
||||
console.log(`[INFO] Switched to connection: ${topicId}`);
|
||||
startStatsInterval();
|
||||
sendCommand(Methods.listContainers);
|
||||
|
||||
Reference in New Issue
Block a user